Skip to content

fix(turso): unbound params executed as NULL — mysql CLI 'commands out of sync'#13

Merged
luthermonson merged 2 commits into
mainfrom
fix/turso-mysql-cli-sync
Jul 21, 2026
Merged

fix(turso): unbound params executed as NULL — mysql CLI 'commands out of sync'#13
luthermonson merged 2 commits into
mainfrom
fix/turso-mysql-cli-sync

Conversation

@luthermonson

Copy link
Copy Markdown
Contributor

Problem

Oracle's mysql 8.4 CLI fails with ERROR 2014 (HY000): Commands out of sync on every statement (even SELECT 1) when the MySQL frontend is backed by litewire-turso. The same CLI works against the rusqlite backend, and PHP mysqli works against both.

Root cause

The mysql >= 8.1 CLI probes dollar-quoting support at startup by sending select $$ and expecting an ERR reply. Byte-level capture of both sessions showed the divergence:

  • rusqlite: rusqlite rejects the bind-count mismatch ($$ parses as one SQLite parameter, zero provided) → ERR packet Wrong number of parameters passed to query. Got 0, needed 1 → CLI continues normally.
  • turso: the Turso engine (0.7.0) executes statements with unbound parameters as NULL, so SELECT $$ returned a 1-column result set with a NULL row. The CLI only checks the probe's return code and never reads the result set, so libmysqlclient's state machine sticks at MYSQL_STATUS_GET_RESULT and every following mysql_real_query fails client-side with CR 2014. mysqli tolerated it because mysqlnd never sends this probe.

Fix

crates/litewire-turso/src/lib.rs: add an SQLite-tokenizer-style placeholder counter (?, ?NNN, :name, @name, $name; skips string literals, quoted identifiers, -- and /* */ comments) and reject bind-count mismatches in query()/execute() after prepare, with the same message shape rusqlite produces. turso 0.7.0 does not expose parameters_count() on its public Statement (it exists only on the private turso_sdk_kit inner), hence the scanner.

Tests

  • param_count_scanner, unbound_parameter_rejected_like_rusqlite, execute_bind_count_mismatch_rejected (litewire-turso unit tests)
  • crates/litewire/tests/mysql_turso_e2e.rs (feature turso): asserts select $$ yields a server ERR packet and the same connection stays usable, plus wire CRUD sanity
  • cargo test --workspace: 25/25 suites pass; cargo clippy --workspace --all-targets -- -D warnings and cargo clippy -p litewire --features turso --all-targets -- -D warnings clean; cargo fmt --check clean

Repro evidence (mysql:8.4 CLI via podman)

Before: ERROR 2014 (HY000) at line 1: Commands out of sync; you can't run this command now
After:

$ mysql -P 33071 -e "SELECT 1"                      # turso backend
1
$ mysql -P 33071 -e "CREATE TABLE t ...; INSERT ...; SELECT * FROM t; SHOW TABLES;"
id  v
1   x
Tables_in_database
t

rusqlite backend behavior unchanged.

… out of sync'

The Turso engine executes statements with unbound parameters as NULL
instead of erroring. Oracle's mysql >= 8.1 CLI probes dollar-quoting
support at startup with `select $$` and requires an ERR reply; against
the turso backend the probe instead returned a one-column result set
(NULL row) that the CLI never reads, wedging libmysqlclient's state
machine so every subsequent statement failed client-side with
CR 2014 "Commands out of sync". The rusqlite backend was immune because
rusqlite rejects a parameter-count mismatch before executing.

Add an SQLite-tokenizer-style placeholder counter to litewire-turso and
reject bind-count mismatches in query()/execute() after prepare, with
the same "Wrong number of parameters passed to query. Got X, needed Y"
message shape rusqlite produces.

Regression coverage: unit tests for the scanner and both backend paths,
plus a wire-level e2e test (feature `turso`) asserting `select $$`
yields a server ERR packet and the connection stays usable.

Verified with mysql:8.4 CLI via podman: before the fix every statement
returned ERROR 2014; after, SELECT/CRUD/SHOW all pass against the turso
backend and rusqlite behavior is unchanged.
…canner

Review differential-testing found $name::type and $name::type(...) were
over-counted (the :: suffix re-entered the :name arm as a phantom
parameter). sqlite3 tokenize.c treats ::seg repetitions and one trailing
(...) as part of the $-variable name; the scanner now does too. Adds the
review's mismatch cases as regression tests plus a retirement TODO for
when turso exposes a public parameter count.
@luthermonson
luthermonson merged commit 9ff1b2b into main Jul 21, 2026
3 checks passed
@luthermonson
luthermonson deleted the fix/turso-mysql-cli-sync branch July 21, 2026 15:05
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