Skip to content

A MySQL-compatible warehouse can be mounted - #315

Merged
WaylandYang merged 2 commits into
devfrom
feat/mysql-wire-protocol
Sep 4, 2026
Merged

WaylandYang merged 2 commits into
devfrom
feat/mysql-wire-protocol

Conversation

@WaylandYang

@WaylandYang WaylandYang commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Closes #303. Also settles the line 0018 left open — that record landed the HTTP family and skipped the MySQL wire family, so the order the query_engine header always described (Postgres, MySQL, HTTP) is now complete.

One driver reaches MySQL, MariaDB, TiDB, OceanBase, Doris and StarRocks: they all speak this protocol. The seam did the work — QueryEngine already fixed the shape, so this is a new file, five lines in the factory and dispatch, and one widened CHECK (migration 0025).

The type table is the whole risk

Postgres hands row_to_json the row and gets JSON back with the column order intact. MySQL has no equivalent, so values are read column by column and the JSON assembled here — which means a table mapping column types to how they are read, and a wrong entry in that table does not raise anything. It silently turns a column into nulls.

The table's authority is the driver's own ColumnType::name and the compatible predicate of each type, not the MySQL manual. Reading those found two entries that would have been wrong:

  • Unsigned integers carry a suffix. The driver reports BIGINT UNSIGNED, not BIGINT, and its i64 decoder explicitly refuses unsigned columns. Matching on the bare names would have sent every unsigned column to the text fallback, which cannot read an integer either — the column would arrive as nulls.
  • DECIMAL has no text or float path. The driver keeps it out of f64 ("floating-point numbers have different semantics") and out of String. BigDecimal is the only way in, which is why sqlx's bigdecimal feature is now on. Without it, money columns — nearly always DECIMAL — would have come back empty.

TINYINT(1) also reports as BOOLEAN rather than TINYINT, which the table follows.

Decimals go out through the same coerce path as the Databricks and Snowflake engines, so a value beyond f64 precision stays a string instead of becoming a plausible-looking approximation.

The other two differences from Postgres

Two spellings for the statement timeout. MySQL uses max_execution_time (milliseconds), MariaDB max_statement_time (seconds). Both are tried and a failure to set either is an error rather than a silent downgrade: this layer is what stops a full-table scan from taking the server down, and the outer LIMIT does not stop it (the scan finishes, then the rows are truncated).

information_schema means the opposite thing. In MySQL a schema is a database, and information_schema is one of the system schemas to exclude rather than the catalog to read through. Empty column comments come back as '' rather than NULL, so they are filtered — otherwise every table would carry a blank comment.

mariadb:// is rewritten to mysql:// for the driver, since it is the same protocol under another name.

Verified

Unit tests cover the type table entry by entry (every integer width signed and unsigned, DECIMAL, BOOLEAN, the four time types, the binary fallback), the scheme rewrite, and the MySQL dialect passing the same SQL gate as the other engines — backticked identifiers parse where the Postgres dialect would reject them.

End to end against a running server: mysql:// and mariadb:// both register as the mysql engine, credentials come back as host:port/db only, and an unsupported scheme is refused with a message that now lists mysql://. Migration 0025 applies and the CHECK holds the new value. cargo fmt --check, cargo clippy -D warnings, cargo test --workspace with UTOPIA_TEST_REQUIRE_DB=1, and pnpm build all pass.

Verified against a running MariaDB 11.4. The registry is unreachable from this machine's Docker daemon (its internal proxy cannot get out), so the image was fetched over the host's own network and side-loaded. That turned the three untested statements into tested ones, and the live check is now a test in the module, guarded by UTOPIA_TEST_MYSQL_URL so it skips where no server is running (#316 carries the setup).

On the live server: SELECT 1 connects; fetch_schema returns both tables with types at full precision (decimal(12,2), bigint(20) unsigned) and column comments attached, with empty comments filtered; and a real round trip types every value correctly — DECIMAL comes back as 1234.56 rather than null, BIGINT UNSIGNED as the full 18446744073709551615, TINYINT(1) as true, DATE as 2023-06-01, and an all-NULL row as nulls in every column. The first two of those are exactly the defects the driver's source predicted, now confirmed both ways.

The MariaDB timeout branch ran for real: MariaDB rejects max_execution_time with ERROR 1193 Unknown system variable, so the fallback to max_statement_time is what let the query through.

What remains for #316 is narrower than it was: MySQL proper (its max_execution_time branch, and its native JSON type — MariaDB aliases JSON to LONGTEXT, so that arm went untaken here), plus the protocol-compatible engines.

🤖 Generated with Claude Code

WaylandYang and others added 2 commits September 5, 2026 02:28
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
@WaylandYang
WaylandYang force-pushed the feat/mysql-wire-protocol branch from a77cd8c to bc84b19 Compare September 4, 2026 18:28
@WaylandYang
WaylandYang merged commit d286556 into dev Sep 4, 2026
4 checks passed
@WaylandYang
WaylandYang deleted the feat/mysql-wire-protocol branch September 4, 2026 18:31
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.

A MySQL-compatible warehouse cannot be mounted

1 participant