Repository navigation
fix(mysql): a MariaDB server read as MySQL, and an unmeasured metric read as 99% (#424) - #458
Merged
Merged
Conversation
…read as 99% (#424) Two defects found while connecting the product to MariaDB 12.3 and MySQL 26.7 side by side, both in the MySQL provider rather than in either engine. The overview named the wrong vendor. `getOverview()` returned `MySQL ${version}` unconditionally, so a MariaDB server whose `VERSION()` answers `12.3.2-MariaDB-ubu2404` was published as "MySQL 12.3.2-MariaDB-ubu2404" - an assertion the server never made. The same statement was wrong for every relative whose version string names itself: TiDB `8.0.11-TiDB-v8.5.1`, Vitess `8.0.43-Vitess` and OceanBase `5.7.25-OceanBase_CE-v4.4.2.1`, all four being the strings WIRE_COMPATIBLE_ENGINES already records from a live probe. A version string that names its own vendor is now passed through unchanged, and only a bare number gets the `MySQL ` prefix. StarRocks and SingleStore are deliberately not on that list: both answer with a plain MySQL number and give nothing to key on, which the compatibility table already records as their behaviour. `getPerformanceMetrics()` manufactured measurements. MariaDB ships `performance_schema` OFF by default (measured on `mariadb:12.3`: `@@performance_schema` = 0) and the tables still exist, so the metric queries do not fail - each is a bare `SELECT (subquery)` that answers one row of NULLs. `parseFloat(row?.hit_ratio || "99")` turned that NULL into a confident 99% cache hit ratio, and the buffer pool and QPS defaults turned theirs into 0. Connected in the browser, a stock MariaDB server therefore reported a healthy 99% cache for a figure nobody had taken. This is the fault #448 and #452 removed from the panels, still live in the provider that feeds them: ABSENCE and ZERO are different inputs, and only the first must stay invisible. Every reading now goes through `measuredNumber()` and a field with nothing behind it is omitted; if `performance_schema` is absent outright the method returns `{}` rather than a set of defaults. `getHealth()` had the same fabricated 99, and now uses the shared `formatCacheHitRatio()`, which is the one place that already turns an absent ratio into "N/A". One thing the fix uncovered: `Innodb_deadlocks` is MariaDB's status variable and MySQL does not publish it. `SHOW STATUS LIKE 'Innodb_deadlocks'` is empty on both MySQL 8.0.46 and 26.7.0, so `parseInt(row?.Value || "0")` had been reporting a deadlock count MySQL never gave. It is now absent there and present on MariaDB, which makes it the one performance figure a default MariaDB reports and a MySQL server does not. Measured after the change, same two connections, same session: MySQL 26.7 version "MySQL 26.7.0" cacheHitRatio 96.49, queriesPerSecond 0.03, bufferPoolUsage 14.89 MariaDB 12.3 version "12.3.2-MariaDB-ubu2404" deadlocks 0, and nothing else claimed Docs and tests move with the code. `docs/providers/mysql.md` gains a section on the MySQL-protocol engines and rewrites the monitoring degradation notes; the compatibility registry and its published table record the `performance_schema` default as measured rather than assumed. Six new provider tests cover the vendor label for all four probed strings, the plain-MySQL case, `performance_schema` off, `performance_schema` absent, and a server that publishes no deadlock counter.
|
cevheri
approved these changes
Aug 22, 2026
cevheri
added a commit
that referenced
this pull request
Aug 23, 2026
…t had done, plus five more (#469) * fix(providers): every MySQL DDL and DML reported a failure for work it had done, plus five more Six defects, each measured against a live server before and after. Four were BACKLOG entries; two were found while closing them and are the most serious of the six. Every non-SELECT statement on MySQL threw. `query()` called `.map` on mysql2's first return value, which is a `ResultSetHeader` object rather than an array for DDL and DML, so `CREATE TABLE`, `INSERT`, `UPDATE` and `DELETE` each answered `result.rows.map is not a function` AFTER the server had applied the statement - the answer that makes a user retry and double-apply it. Measured through the provider with an interleaved SELECT proving each statement had landed, and again in the browser: a `CREATE TABLE` from the editor now reports "0 rows" with the table created, and a two-row `INSERT` reports "2 rows". The tests could not see it because they mocked `execute` as returning an array; the mock type is now wide enough to express the real shape, which is what hid it. X7 - the DDL export typed a numeric or a timestamp column as TEXT, because the wire hands it a string. The inference was never at fault: it never saw a type. `pg`, `mysql2`, `oracledb` and `mssql` now fill `QueryResult.columnTypes` with the engine's own spelling, joining the six provider families that already did. The mappings were measured, not remembered: Postgres resolves its OIDs through a generated `pg_type` table (149 built-ins; verified over dvdrental as 128 columns, 125 named, 0 wrong), and MySQL's shared type codes are separated by charset 63 and the length tiers rather than by assumption (38 of 39 match `information_schema.DATA_TYPE`; `POINT` and `GEOMETRY` are indistinguishable on the wire). In the browser a Postgres result now reads `price numeric`, `ts timestamp without time zone`, `b bytea` in the grid, and its exported DDL says the same instead of TEXT. That change had a consequence which is fixed in the same PR rather than left to be discovered: a declared type is a BARE base name, because a length cannot be recovered from the wire, and a bare name is not legal DDL everywhere. Measured by replay - MySQL `ERROR 1064`, Oracle `ORA-00906`, and SQL Server accepting it while silently narrowing `nvarchar` to length 1. The export now completes a bare name to its target dialect's unbounded form, and re-spells a foreign engine's declaration rather than writing a `VARCHAR2` into Postgres DDL, which the shells make reachable by keeping the connection type beside the tab's own result. Every arm replayed into its own engine and read back out of its catalog. X8 - the SQL INSERT and DDL exports wrote a binary value as its JSON shape, so a replayed INSERT stored 46 bytes of `{"type":"Buffer","data":[...]}` where six bytes belonged, successfully and silently. Each dialect now writes a literal its own engine accepts, every spelling measured rather than assumed: Postgres's `X'...'` is a bit string and cannot be cast to bytea, MySQL has no `0x` spelling for an empty value, SQL Server rejects the standard form, and Oracle parses neither so the conversion function is the literal. Replayed into seven engines, six bytes each. D7 - three providers answered the performance panel with a fabricated cache hit ratio, and the sweep found five more. All eight are honest now: MySQL's health read no longer dies on a tenant with no `performance_schema` DATABASE (measured on OceanBase as ERROR 1049, where the entry's own three sites had already been fixed by #458), and SQLite, LibreDB, MongoDB, Couchbase, Postgres, Oracle and SQL Server report an unmeasurable ratio as absent, which #452's tabs already render as "Not measured". Two of those fabrications were pinned by tests - one named "reports zero rather than a perfect score when denied" - and a denial is the ordinary case, so the panel was inventing a red critical cache fault. `bufferPoolUsage` went with them in three providers: it was the cache hit ratio wearing a second name, drawn and rated as an independent gauge. In the browser: MySQL 98.8%, Oracle 99.5%, SQLite "Not measured" where it used to claim a fixed 95%. D12 - a pasted `rediss://` URL landed on SSL mode `disable`, so the scheme was silently dropped. It now arrives as `require`, and so does `couchbases://`, whose excuse for being left out ("the provider re-reads the scheme") was untrue: that transport picks http or https from `config.ssl` alone and was posting plaintext to 18091. `mongodb+srv://` is deliberately left unset - the driver turns TLS on itself WITH chain verification, and carrying our `require` there would have stopped an Atlas certificate being checked. Verified in the browser and end to end against a TLS-only Redis, both arms. Follow-ups recorded rather than smuggled in: D13 (Oracle answers every non-SELECT with `rowCount: 0`, including rows it just wrote), D14 (what a real buffer-pool gauge would cost on each engine), D15 (SQLite's table size is `rowCount * 100`), D16 (the TLS a connection string carries in its query string is still dropped), X9 (what `columnTypes` still cannot name) and X10 (MySQL and Cassandra stringify their bytes before the export can see them, which bounds what X8 closed). * docs(export): the reason a dialect is absent from STANDS_ALONE covered only half the cases An external review of #469 approved it and read the absence of the other dialects as settled. It is not, and the module's own comment is why: it justified having no row for clickhouse/trino/sqlite/cassandra by saying those engines spell their types out in full, which is true of the names THEY declare and says nothing about a name arriving from somewhere else. Measured under each target for one Oracle result: postgres writes TEXT, mysql TEXT, mssql NVARCHAR(MAX) - and clickhouse, plus the no-connection default, write VARCHAR2, BINARY_DOUBLE and CLOB, which replay nowhere. The no-connection case is a regression: a value-shaped guess used to write portable standard SQL there. Widening the rule is not the fix - Trino's own bare varchar is legal and unbounded and would become a TEXT it does not have - so each remaining dialect needs a measured row, recorded as BACKLOG X11. Comment and entry only, no behaviour change.
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.



Found while connecting the product to MariaDB 12.3 and MySQL 26.7 side by side, to check the claims a page for the MariaDB documentation would have to make. Both defects are in our MySQL provider, not in either engine.
The overview named the wrong vendor
getOverview()returnedMySQL ${version}unconditionally. A MariaDB server whoseVERSION()answers12.3.2-MariaDB-ubu2404was published as "MySQL 12.3.2-MariaDB-ubu2404" — an assertion the server never made, and the first thing anyone opening the product against MariaDB would see.The same statement was wrong for every relative whose version string names itself. All four are strings
WIRE_COMPATIBLE_ENGINESalready records from a live probe:VERSION()12.3.2-MariaDB-ubu24048.0.11-TiDB-v8.5.18.0.43-Vitess5.7.25-OceanBase_CE-v4.4.2.1A version string that names its own vendor is now passed through unchanged; only a bare number gets the
MySQLprefix. StarRocks and SingleStore are deliberately not on that list — both answer with a plain MySQL number and give nothing to key on, which the compatibility table already records as their behaviour.An unmeasured metric read as 99%
MariaDB ships
performance_schemaOFF by default. Measured onmariadb:12.3,@@performance_schema= 0, and the tables still exist — so the metric queries do not fail. Each is a bareSELECT (subquery)that answers one row of NULLs.parseFloat(row?.hit_ratio || "99")turned that NULL into a confident 99% cache hit ratio, and the buffer-pool and QPS defaults turned theirs into 0. Connected in the browser, a stock MariaDB server reported a healthy 99% cache for a figure nobody had taken.This is the fault #448 and #452 removed from the panels, still live in the provider that feeds them. ABSENCE and ZERO are different inputs, and only the first must stay invisible. Every reading now goes through
measuredNumber(), a field with nothing behind it is omitted, and ifperformance_schemais absent outright the method returns{}rather than a set of defaults.getHealth()carried the same fabricated 99 and now uses the sharedformatCacheHitRatio().What the fix uncovered
Innodb_deadlocksis MariaDB's status variable. MySQL does not publish it:SHOW STATUS LIKE 'Innodb_deadlocks'is empty on both 8.0.46 and 26.7.0, measured. SoparseInt(row?.Value || "0")had been reporting a deadlock count MySQL never gave. It is now absent there and present on MariaDB, which makes it the one performance figure a default MariaDB reports and a MySQL server does not.Measured after the change
Same two connections, same session, through
/api/db/monitoring:Checks
Written test-first: the six new tests failed with
Received: "MySQL 12.3.2-MariaDB-ubu2404"andReceived: 99before the implementation landed. They cover the vendor label for all four probed strings, the plain-MySQL case,performance_schemaoff,performance_schemaabsent outright, and a server that publishes no deadlock counter.format,lint,typecheck,knip,testandbuildall pass locally; merged coverage stays at 100% (40321/40321 lines).Docs move with the code, per the provider triad rule:
docs/providers/mysql.mdgains a section on the MySQL-protocol engines and rewrites the monitoring degradation notes, and both the compatibility registry and its published table record theperformance_schemadefault as measured rather than assumed.