Skip to content

compat: session/transaction rewrites + real MySQL/PG error codes + CI - #4

Merged
luthermonson merged 2 commits into
mainfrom
compat/session-statements-and-ci
Jul 10, 2026
Merged

compat: session/transaction rewrites + real MySQL/PG error codes + CI#4
luthermonson merged 2 commits into
mainfrom
compat/session-statements-and-ci

Conversation

@luthermonson

Copy link
Copy Markdown
Contributor

Summary

  • Adds the first CI workflow (fmt / clippy -D warnings / test on ubuntu-latest).
  • Fixes every warning main currently emits so -D warnings passes.
  • Adds MySQL / T-SQL session and transaction compatibility that PDO / real clients need.
  • Maps SQLite errors to real MySQL error codes + SQLSTATEs (and the PG equivalents on the other frontend).
  • Caps per-connection prepared statements to 1024 and returns MySQL 1461 on overflow.
  • README truth pass.

Base branch: main. The follow-up perf/translate-cache-and-prepare branches from this one.

Changes

CI (.github/workflows/ci.yml)

Three jobs on GitHub-hosted ubuntu-latest: rustfmt check, clippy -D warnings, workspace tests. Uses dtolnay/rust-toolchain@stable + Swatinem/rust-cache@v2.

Warning cleanup

  • Drop the unused sqlparser::Expr import in translate/mysql.rs.
  • Drop the dead DONE_MORE const in tds/token.rs.
  • Delete the unread PreparedStmt.param_count field.
  • Collapse redundant closures on the pgwire portal parameter extractions.
  • Fix manual_ignore_case_cmp, manual_strip, useless_conversion, single_match, and one too_many_arguments (annotated in place — the token writer's arity is inherent to the TDS packet layout).
  • metadata::extract_where_value + the trim_offset local are the owner's WIP scaffolding; kept in place with #[allow(dead_code)] and a _ prefix, per the audit note.

Session / transaction compat (litewire-translate)

Handled textually before the sqlparser step so cross-dialect grammar quirks don't matter:

  • START TRANSACTION [READ ONLY | READ WRITE | WITH CONSISTENT SNAPSHOT] -> BEGIN. This is the concrete fix for PDO::beginTransaction().
  • T-SQL BEGIN TRANSACTION [name] -> BEGIN (name stripped -- SQLite has no named transactions).
  • COMMIT WORK / COMMIT TRANSACTION [name] -> COMMIT.
  • ROLLBACK WORK / ROLLBACK TRANSACTION [name] -> ROLLBACK.
  • ROLLBACK TRANSACTION TO [SAVEPOINT] name -> ROLLBACK TO SAVEPOINT name.
  • SAVEPOINT / RELEASE SAVEPOINT / ROLLBACK TO SAVEPOINT verified passthrough via new tests.
  • SET autocommit = 1|ON|true -> noop with debug!. SET autocommit = 0|OFF|false -> noop with warn! (litewire does not emulate MySQL's implicit-transaction mode, and half-emulating it via BEGIN would give worse semantics than a visible warning).
  • SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL ... -> noop (SQLite is effectively serializable).
  • SET @@x, SET @@session.x, SET @@global.x normalized to their plain-name form so downstream matching works.
  • LOCK TABLES / UNLOCK TABLES -> noop with warn! (SQLite gives file-level locking only; documenting rather than silently faking).

Function rewrites (litewire-translate::common)

  • LAST_INSERT_ID() -> last_insert_rowid(). The 2-arg MySQL form (LAST_INSERT_ID(expr) sets a session value) is intentionally left to fail loudly at SQLite arity check rather than silently changing semantics.
  • ROW_COUNT() -> changes().
  • DATABASE() / SCHEMA() -> 'main'.
  • VERSION() -> '8.0.0-litewire' (matches the @@version metadata fast path).
  • USER() / CURRENT_USER() / SESSION_USER() / SYSTEM_USER() -> 'root@localhost'.
  • CONNECTION_ID() -> 0.
  • Known gap (not in this PR, deliberately): FOUND_ROWS() / SQL_CALC_FOUND_ROWS -- both need session state (per-connection row-count carry-over) that the current handler doesn't track.

Error-code fidelity

  • New litewire-mysql::error_map classifies backend error strings into real MySQL codes:
    • SQLITE_BUSY / SQLITE_LOCKED -> 1205 ER_LOCK_WAIT_TIMEOUT (HY000).
    • SQLITE_CONSTRAINT_UNIQUE / PRIMARYKEY -> 1062 ER_DUP_ENTRY (23000).
    • SQLITE_CONSTRAINT_FOREIGNKEY -> 1452 ER_NO_REFERENCED_ROW_2 (23000).
    • SQLITE_READONLY -> 1290 ER_OPTION_PREVENTS_STATEMENT (HY000).
    • Everything else -> 1105 ER_UNKNOWN_ERROR with the raw text. SQLSTATE is derived from ErrorKind::sqlstate() by opensrv-mysql -- the SQLSTATE field on MysqlError is retained (test-only, gated by #[cfg_attr(not(test), allow(dead_code))]) to pin those mappings in unit tests.
  • New litewire-postgres::error_map:
    • unique -> 23505, FK -> 23503, not-null -> 23502, check -> 23514, busy -> 55P03, readonly -> 25006, else XX000.
  • Both classifiers have full unit-test coverage.

Prepared-statement cap

Per-connection cap of 1024. Overflow returns MySQL 1461 (ER_MAX_PREPARED_STMT_COUNT_REACHED) with the standard message. Trivial affected-rows / last-insert-id cast fixes routed through .max(0) + TryFrom rather than i64 as u64.

README truth pass

  • Dropped the unverified "Tested with WordPress/Laravel/Drupal/DBeaver/pgAdmin/..." list. Replaced with an accurate Compatibility section: mysql_e2e.rs exercises the wire protocol end-to-end; PG/TDS are wire-compatible for CRUD only; TDS marked experimental (auth simplified, no SSL). Called out the --features postgres,tds build-time gate on the multi-frontend command.
  • All CLI flag names verified against crates/litewire/src/main.rs: they were already correct (--mysql-listen, --postgres-listen, --tds-listen, --hrana-listen) -- no --pg-listen typo existed. Left them alone.
  • docs/sql-translation.md isn't linked from anywhere in the current README; the sole link (docs/architecture.md) does exist. No stale doc-link removal needed.
  • TDS TOP n -> LIMIT n is already implemented (tds::rewrite_top_to_limit) and tested; the README's claim is accurate.

Test plan

  • cargo test --workspace -- 271 passing (172 translate lib incl. all new session/transaction tests, 25 mysql lib incl. error_map, 23 postgres lib incl. error_map, 20 tds lib, 21 in-process mysql_e2e wire tests, 9 translate integration inside litewire, 23 backend, 1 doctest). 0 failures.
  • cargo clippy --workspace --all-targets -- -D warnings -- clean.
  • cargo fmt --all -- --check -- clean (reformatted the tree so CI can enforce it going forward; that's the bulk of the diff).

Adds a first CI workflow (fmt / clippy -D warnings / test on ubuntu-latest)
and the fixes that were needed to make it green today.

Compiler + clippy hygiene
- Drop unused sqlparser::Expr import in translate/mysql.rs
- Drop dead DONE_MORE constant in tds/token.rs
- Remove unread PreparedStmt.param_count in mysql/handler.rs
- Collapse redundant closures on pgwire portal parameter extraction
- Fix manual case-insensitive cmp, manual strip, useless conversion,
  single-match, and too-many-args lints across the workspace
- extract_where_value / trim_offset in metadata.rs marked as WIP
  scaffolding (underscored / #[allow(dead_code)]) rather than removed --
  they are part of an in-progress WHERE-clause extraction rework

Session / transaction compatibility (litewire-translate)
- START TRANSACTION [READ ONLY|READ WRITE|WITH CONSISTENT SNAPSHOT] -> BEGIN
- BEGIN [WORK|TRANSACTION [name]] -> BEGIN (fixes PDO::beginTransaction)
- COMMIT / ROLLBACK [WORK|TRANSACTION [name]] normalized
- ROLLBACK TRANSACTION TO [SAVEPOINT] name rewritten to ROLLBACK TO SAVEPOINT
- SAVEPOINT / RELEASE SAVEPOINT / ROLLBACK TO SAVEPOINT verified passthrough
- SET autocommit = 1|ON|TRUE|0|OFF|FALSE -> noop (with WARN on 0/OFF/FALSE
  since litewire does not emulate MySQL implicit transactions)
- SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL ... -> noop with debug log
- SET @@[session|global.]var forms routed to their plain-name handling
- LOCK TABLES / UNLOCK TABLES -> noop with warn
- LAST_INSERT_ID() -> last_insert_rowid(); ROW_COUNT() -> changes()
- DATABASE() / VERSION() / USER() / CURRENT_USER() / SESSION_USER() /
  SYSTEM_USER() / CONNECTION_ID() -> constant projections that match the
  values used by the @@variable metadata fast path
- SQL_CALC_FOUND_ROWS / FOUND_ROWS() intentionally NOT implemented -- would
  require session state; documented as a known gap.

Error-code fidelity
- New litewire-mysql::error_map: classifies backend error strings to real
  MySQL codes: SQLITE_BUSY -> 1205, unique/PK -> 1062 (23000), FK -> 1452
  (23000), readonly -> 1290, else 1105.
- New litewire-postgres::error_map: unique -> 23505, FK -> 23503, not-null
  -> 23502, check -> 23514, busy -> 55P03, readonly -> 25006, else XX000.
- Both classifiers unit-tested.

Prepared-statement cap
- Per-connection cap of 1024 prepared statements; overflow returns MySQL
  error 1461 (ER_MAX_PREPARED_STMT_COUNT_REACHED). Protects the server
  against clients that never send COM_STMT_CLOSE.

Cast safety
- Route affected_rows / last_insert_id through .max(0) + TryFrom rather than
  reinterpret-casting negative i64 into u64.

README truth pass
- Replace unverified "Tested with WordPress/Laravel/..." with an accurate
  Compatibility section: mysql_e2e covers the wire protocol end-to-end;
  postgres/tds are wire-compatible for basic CRUD; TDS marked experimental
  (auth simplified, no SSL). Note that postgres+tds require build-time
  features. All flag names verified against crates/litewire/src/main.rs.
CI formats this repo standalone with stable rustfmt defaults; local
checkouts nested under other projects were inheriting a parent
rustfmt.toml and disagreeing with CI. Pin the config in-repo so both
always match.
@luthermonson
luthermonson merged commit 88d534a into main Jul 10, 2026
3 checks passed
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