Skip to content

feat(mysql): FROM DUAL, index hints, locking reads, unbounded LIMIT, option-only ALTER - #56

Merged
luthermonson merged 1 commit into
mainfrom
feat/mysql-compat-statements
Oct 23, 2026
Merged

luthermonson merged 1 commit into
mainfrom
feat/mysql-compat-statements

Conversation

@luthermonson

Copy link
Copy Markdown
Contributor

MySQL statement shapes that PHP apps (Sendy among them) send and that SQLite rejects as written. Each was verified over the MySQL wire against rusqlite and Turso: crates/litewire/tests/mysql_compat_statements.rs checks results and affected-row counts, not the emitted SQL.

MySQL Now
INSERT ... SELECT ... FROM DUAL WHERE NOT EXISTS (...) FROM DUAL dropped. 1 affected row when inserted, 0 when suppressed (mysqli_affected_rows is branched on). A quoted or aliased dual is kept as a user table
FORCE / USE / IGNORE INDEX|KEY [FOR JOIN|ORDER BY|GROUP BY] (...) removed. Never mapped to INDEXED BY, which errors when the index is missing
FOR UPDATE, FOR SHARE, NOWAIT, SKIP LOCKED, LOCK IN SHARE MODE removed, rows returned. LOCK IN SHARE MODE is stripped before parsing because sqlparser cannot parse it. Single-writer semantics are documented: a double "claim" fails at the second write rather than blocking
LIMIT 18446744073709551615 [OFFSET n] / LIMIT n, 18446744073709551615 (any literal ≥ 2^63) LIMIT -1
ALTER TABLE t ROW_FORMAT=… / ENGINE=… / [DEFAULT] CHARSET … / COLLATE … / AUTO_INCREMENT=… / COMMENT … / CONVERT TO CHARACTER SET … / ENABLE|DISABLE KEYS / FORCE / ALGORITHM= / LOCK= (options only, comma- or space-separated) no-op. If mixed with a real operation, the statement goes to the parser
LEAST / GREATEST / CHAR_LENGTH / CHARACTER_LENGTH min / max (multi-arg, NULL if any NULL) / length

Fix: the DDL display-width pre-pass and the SELECT-hint pre-pass pushed every byte as a char, so multi-byte UTF-8 was re-encoded (DEFAULT 'café' was stored as 'café', and the same happened to strings in a SELECT SQL_NO_CACHE ...). They also ended a string at a backslash-escaped quote. Both pre-passes now copy byte spans through a shared quote scanner that understands escapes.

Docs: docs/sql-translation.md (MySQL dialect + "what does not work"), CHANGELOG.

Local: clippy (default and all features), +1.88 check, cargo test --workspace --all-features. The only failure is the Windows-only single_tenant_attach path test, which fails on main too.

…option-only ALTER

MySQL statement shapes PHP applications use that SQLite rejects as written:

- `... FROM DUAL` drops the FROM, so `INSERT ... SELECT ... FROM DUAL WHERE
  NOT EXISTS (...)` works and reports 0 affected rows when suppressed.
- Index hints (FORCE/USE/IGNORE INDEX|KEY [FOR ...]) are removed, never
  mapped to INDEXED BY (which fails on a missing index).
- Locking reads (FOR UPDATE / FOR SHARE / NOWAIT / SKIP LOCKED, and the
  unparseable LOCK IN SHARE MODE) are removed; single-writer semantics
  documented.
- LIMIT literals >= 2^63 (MySQL's 18446744073709551615 "no limit") become
  LIMIT -1.
- ALTER TABLE with only table options (ROW_FORMAT, ENGINE, CHARSET,
  COLLATE, AUTO_INCREMENT, COMMENT, CONVERT TO CHARACTER SET,
  ENABLE/DISABLE KEYS, FORCE, ...) is a no-op.
- LEAST/GREATEST -> multi-arg min/max, CHAR_LENGTH -> length.

Fixes the DDL display-width and SELECT-hint pre-passes, which re-encoded
each byte of multi-byte UTF-8 ('café' -> 'café') and ended a string at a
backslash-escaped quote.
@luthermonson
luthermonson merged commit bb409f4 into main Oct 23, 2026
5 checks passed
@luthermonson
luthermonson deleted the feat/mysql-compat-statements branch October 23, 2026 04:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant