fix(mysql): one server version for the handshake, VERSION() and @@version - #30
Merged
Conversation
…sion A client can ask litewire its version three ways -- the wire handshake, SELECT VERSION(), and @@Version -- and each was answered by its own string literal. The handshake said 8.0.36-litewire; the other two said 8.0.0-litewire. Clients that branch on version got different answers depending on which one they asked. All three now read litewire_translate::SERVER_VERSION. Unified upward on the handshake's value: the handshake is what every client reads at connect and may cache, so changing it has the wider blast radius, and 8.0.36 is the more accurate string. Three existing assertions encoded the old divergent value and are updated to the new one. This is a deliberate behaviour change, not a test weakening: each one now asserts against the shared constant as well as the literal, so a future drift fails the test rather than silently passing. Fixing this surfaced that SELECT VERSION() never worked at all. The session-identity built-ins were rewritten to a one-argument coalesce(), which SQLite rejects at prepare time -- "wrong number of arguments to function coalesce()". VERSION, DATABASE, SCHEMA, USER, CURRENT_USER, SESSION_USER, SYSTEM_USER and CONNECTION_ID were all affected. The unit tests missed it because they only inspected the emitted SQL string. Constants now go through constant_text/constant_number, which use the single-argument scalars SQLite does accept, and the new end-to-end tests execute the statements rather than reading the SQL back. Fixes #21
luthermonson
added a commit
that referenced
this pull request
Sep 1, 2026
…sion (#30) A client can ask litewire its version three ways -- the wire handshake, SELECT VERSION(), and @@Version -- and each was answered by its own string literal. The handshake said 8.0.36-litewire; the other two said 8.0.0-litewire. Clients that branch on version got different answers depending on which one they asked. All three now read litewire_translate::SERVER_VERSION. Unified upward on the handshake's value: the handshake is what every client reads at connect and may cache, so changing it has the wider blast radius, and 8.0.36 is the more accurate string. Three existing assertions encoded the old divergent value and are updated to the new one. This is a deliberate behaviour change, not a test weakening: each one now asserts against the shared constant as well as the literal, so a future drift fails the test rather than silently passing. Fixing this surfaced that SELECT VERSION() never worked at all. The session-identity built-ins were rewritten to a one-argument coalesce(), which SQLite rejects at prepare time -- "wrong number of arguments to function coalesce()". VERSION, DATABASE, SCHEMA, USER, CURRENT_USER, SESSION_USER, SYSTEM_USER and CONNECTION_ID were all affected. The unit tests missed it because they only inspected the emitted SQL string. Constants now go through constant_text/constant_number, which use the single-argument scalars SQLite does accept, and the new end-to-end tests execute the statements rather than reading the SQL back. Fixes #21
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A MySQL client can ask litewire what version it is three ways, and each was answered by its own unrelated string literal:
mysqli_get_server_info(),PDO::ATTR_SERVER_VERSION8.0.36-litewireSELECT VERSION()$wpdb->db_version()and friends8.0.0-litewire@@version8.0.0-litewireAll three now read
litewire_translate::SERVER_VERSION.Unified upward, on the handshake's value
The handshake is what every client reads at connect and may cache for the life of the connection, so changing it moves the answer for every client whether or not it ever calls
VERSION(). Fixing the two emulated paths instead changes only what those functions return. Lower blast radius, and8.0.36is the more accurate string — 8.0.0 was a development milestone release, never GA.The constant lives in
litewire-translatebecause that crate owns two of the three paths andlitewire-mysql(which owns the third) already depends on it. It is backed by a smallserver_version!()macro so the@@versionfast path, which builds SQL literals at compile time withconcat!and needs the value quoted, can reach the same source of truth rather than keeping a second copy.Existing assertions updated — why this is a fixture correction
Three assertions encoded the old, divergent value. They are the behaviour this issue asks us to change, so updating them is not a weakening:
litewire-translate/src/common.rs:466(version_rewrite)litewire-translate/src/metadata.rs:641(system_variables_sql)litewire-translate/src/metadata.rs:739(known_system_variables_produce_expected_values)Each keeps asserting exactly what it asserted before, and the first two now additionally assert against
crate::SERVER_VERSIONrather than only a literal — so if a future change moves the constant without moving the emulation, the test fails instead of quietly passing. Coverage went up, not down. Every edit carries an inline comment naming the issue.SELECT VERSION()had never workedWriting an end-to-end test for the unification turned up a second defect. The statement did not return the wrong version — it returned an error:
The rewrite pass only ever sees the
Functionnode, never its parentExpr, so a built-in with no SQLite analogue cannot become a bare literal — it has to stay a function call. The call chosen wascoalesce(), which SQLite requires to have at least two arguments and rejects at prepare time with one.VERSION,DATABASE,SCHEMA,USER,CURRENT_USER,SESSION_USER,SYSTEM_USERandCONNECTION_IDwere all emitted this way, so none of them has ever executed.The constraint was already known in this exact file: the
FOUND_ROWSarm sitting between the broken ones carries the comment "abs(0): single-argument scalar that SQLite accepts (coalesce and max need >= 2 args)". Its neighbours were never given the same treatment, and the unit tests did not catch it because they assert on the emitted SQL string and never run it.Constant folding now goes through two helpers —
constant_text(usestrim(), which returns a whitespace-free constant unchanged) andconstant_number(usesabs(), matching the existingFOUND_ROWSprecedent) — so the single-argument invariant is stated once and cannot be got wrong per-arm again. No existing test needed changing for this part; all 307 translate tests pass unmodified.This is in scope rather than a separate PR because unifying the answer of a statement that cannot execute is not a fix, and the test that proves the unification is the same test that exposed it.
Testing
cargo test --workspaceandcargo test --workspace --all-featurespass.cargo clippy --workspace --all-targets --all-features -- -D warningsclean;cargo +nightly fmt --all -- --checkclean.crates/litewire/tests/mysql_server_version.rs, running against the rusqlite backend over the real wire protocol:handshake_version_function_and_system_variable_all_agree— assertsSELECT VERSION()andSELECT @@versionboth equalSERVER_VERSION, thatmysql_async's handshake-parsed triple is(8, 0, 36), and that the triple is a prefix of the constant. That last assertion is what ties the wire handshake to the emulated paths, so a fourth path or a re-hardcoding gets caught.session_identity_builtins_execute— runsDATABASE(),SCHEMA(),USER(),CURRENT_USER(),SESSION_USER(),SYSTEM_USER(),CONNECTION_ID()and aVERSION()inside a larger expression, and checks the values. Every one of these fails onmainwith thecoalesce()arity error.Fixes #21