fix(mysql): read SHOW STATUS bare and measure the EXPLAIN grammar at connect (#573, #574) - #599
Merged
Merged
Conversation
getHealth(), getOverview() and getPerformanceMetrics() asked for one status
variable at a time with SHOW STATUS LIKE '<name>'. Measured 2026-09-06 over
mysql2 3.24.2's text protocol against apache/doris:all-in-one-4.1.3, that
clause is a parse error there: errno 1105, ER_UNKNOWN_ERROR, HY000,
"mismatched input 'LIKE' expecting {<EOF>, ';'}(line 1, pos 12)", while a bare
SHOW STATUS is accepted and answers 0 rows. So the Overview and Health panels
failed outright on Doris for a filter the statement does not need (#573).
Each method now sends one bare SHOW STATUS and picks the rows it wants by
Variable_name, matched case-insensitively the way LIKE matched. getOverview()
takes Uptime and Threads_connected out of the same result, one round trip
fewer than before. SHOW VARIABLES LIKE 'max_connections' stays: Doris rejects
the clause on SHOW STATUS only, so the change touches only what a grammar
refuses.
The fabricated fallbacks go with it. Measured the same day, the status lists
differ: MySQL 26.7.0 528 rows, MariaDB 12.3.2 571, SingleStore 75, TiDB 8.5.1
13, StarRocks 3.3.22 0, Doris 4.1.3 0. TiDB publishes Uptime and not
Threads_connected; StarRocks and Doris publish neither. An unpublished
variable is now an absent reading: activeConnections is omitted from both
HealthInfo and DatabaseOverview, and startTime is omitted with uptime "N/A".
maxConnections stays a required number because 0 there means "no limit
published", but its || "151" default is gone, which reported MySQL's
compiled-in ceiling for every server that publishes none.
Docs and tests move with the code: mysql.md section 8 states the absence
contract and the measured row counts, and the provider test file drives the
Doris shape from probe-doris.json.
…able The paragraph was inserted between two rows of the section 8 method table. In GFM a blank line and a paragraph end a table, so the five rows after it, from getSlowQueries() to getStorageStats(), rendered as literal pipe text. It now sits after the last row, before the graceful-degradation list, and its two em-dashes are commas so the new prose matches the house style.
The MySQL-wire relatives that refuse EXPLAIN FORMAT=JSON accept a plain
EXPLAIN and answer their own text or tabular plan. Measured 2026-09-06 over
mysql2 3.24.2 on the text protocol: TiDB v8.5.1 answers errno 1105 "explain
format 'json' is not supported now", StarRocks 3.3.22 and SingleStore 0.2.82
answer errno 1064, Apache Doris 4.1.3 answers errno 1105 "mismatched input
'=' expecting {<EOF>, ';'}". All four accept the bare EXPLAIN.
The new strategy is shape-driven, never engine-driven: the first column is
the node text, the remaining non-empty columns are the detail, and the
leading run of whitespace and box glyphs on the first column is the nesting.
Doris names its single column "Explain String(Nereids Planner)" and StarRocks
names it "Explain String", which is why nothing here keys on a column name.
An estRows column that parses as a finite number becomes the only metric; no
other number is invented. Blank rows are dropped, StarRocks pads its plan
with two and Doris with six.
The render model's raw is the plain text of the plan, so VisualExplain's raw
tab prints a string verbatim instead of stringifying it into one quoted line
with escaped newlines. plan-summary keeps no reading for the format: a text
plan carries no field the agent layer can read, and unknown is the honest
answer.
POST /api/db/query gains an optional "explain": { "mode": "estimate" |
"analyze" }. When it is present the route resolves the CONNECTED provider's
capabilities, builds the statement with that format's strategy, runs that
statement and names the format back in "explainFormat". The raw sql is never
run for an explain request: no strategy or supportsExplain false is a 400
"This server does not support EXPLAIN", a statement the strategy declines is a
400 "Only SELECT statements can be explained", and an explain request beside a
non-empty params array is a 400, since an explain run describes a statement
rather than running one with values bound into it.
The browser stops building EXPLAIN text. Both explain fetches in
use-query-execution now post the user's own statement plus the mode they use
today (analyze for the direct run, estimate for the background pre-warm), and
store the plan under the format the response names, falling back to the static
strategy when the response names none or names one this build does not
register. The pre-flight refusal stays client side, so a non-SELECT still shows
its toast and makes no request; a parameterized run now makes no plan request
at all, because the route refuses that combination.
Measured 2026-09-06 over the MySQL wire protocol (mysql2 3.24.2, text
protocol): EXPLAIN FORMAT=JSON SELECT 1 is errno 1105 on TiDB v8.5.1 ("explain
format 'json' is not supported now") and Apache Doris 4.1.3 ("mismatched input
'='"), errno 1064 on StarRocks 3.3.22 and SingleStore, while plain EXPLAIN
SELECT 1 is accepted on all of them. POST /api/db/provider-meta never connects
(#457), so the client cannot be told which form to build; the connected
provider is the only place that knows.
…ment The route refused an explain request that carried bound params. That is the opposite of the contract PR #304 relies on: every strategy's buildSql only prefixes the statement, so the built statement carries the same placeholders in the same order and the same values bind them. The refusal took the pre-warmed plan away from every generated statement that sends its values separately (#290), so it is gone and the hook sends params with the background plan request again, exactly as before. Also guards the format the response names. getExplainStrategy indexes an object literal, so an inherited key such as "constructor" resolved to a truthy value that is not a strategy and reading .format off it threw. A registered strategy names itself, so that identity is now the guard, with a test for both the unregistered format and the inherited key.
…gistered format with a 400
Two review findings on the mysql-text strategy and the query route.
readEstRows() ran Number() over the raw cell, and Number("") is 0, so a row
whose estRows the engine left blank rendered a "~0 rows" badge nobody measured.
A blank cell is now an absent reading, per the absence rule (#477). The indent
is now the length the prefix strip removes, with no unreachable null arm.
The route compared the resolved strategy with null, but getExplainStrategy()
indexes a Record and answers undefined for a format outside the union, so an
external provider declaring one reached strategy.buildSql and surfaced as a
TypeError 500 instead of the 400 sentence the user can act on.
… a capability (#574) The provider declared explainFormat: "mysql-json" for every engine on this type id, so the Explain panel always sent EXPLAIN FORMAT=JSON. That is MySQL's own grammar and the wire family does not share it. Measured 2026-09-06 through mysql2 3.24.2 over the text protocol: TiDB 8.5.1 and Apache Doris 4.1.3 refuse it with errno 1105, StarRocks 3.3.22 and SingleStore with errno 1064, Databend 1.2.925 with errno 1105 SyntaxException, and every one of them accepts a plain EXPLAIN SELECT 1. MySQL 26.7.0, MariaDB 12.3.2, Vitess 24.0.2 and OceanBase CE 4.4.2 accept both. connect() now probes on the pooled connection it already holds: EXPLAIN FORMAT=JSON SELECT 1 first, then EXPLAIN SELECT 1, and the first that succeeds names the format getCapabilities() declares. The family shares no errno for a grammar refusal, so the probe reads success or failure and never the code, which also keeps src/lib/db free of a branch on the product name. A refusal is a fact about the Explain panel, not the connection, so it never becomes a ConnectionError. Before connect() the provider still answers mysql-json, which is what the type-driven provider-meta route reads and what it read before. The marketplace copy gate derived its explain-capable engine set by grepping provider sources for a literal explainFormat line. It now reads the measured form too, so MySQL stays nameable in a listing's explanation claim.
…6-09-06 re-measurement The nine MySQL-wire relatives were re-measured on 2026-09-06, at the wire with mysql2 3.24.2 and then in a browser against the built app. This sweep makes the registry and the docs say what those runs found. Apache Doris moves from partial to full. Both surfaces that used to fail were ours: the overview and health panels sent SHOW STATUS LIKE, which the Doris grammar rejects, and the Explain panel sent EXPLAIN FORMAT=JSON, which it rejects too. All fifteen answer now. The overview publishes uptime N/A and Connections N/A, not published, because Doris answers a bare SHOW STATUS and SHOW VARIABLES LIKE 'max_connections' with zero rows each. StarRocks stays partial: health, sessions and the monitoring dashboard have no information_schema.PROCESSLIST, which is the engine's own. Its overview no longer fabricates 0/151, and its Explain panel renders StarRocks's own text plan, 14 rows drawn as a 13-node tree. TiDB stays full: its Explain panel renders TiDB's own operator tree with estRows as the row estimate, and the connection count reads not published because its SHOW STATUS has 13 rows and no Threads_connected among them. SingleStore stays partial (row counts and sizes are zeros in information_schema.TABLES) and its Explain panel renders the engine's own text plan; EXPLAIN FORMAT=JSON is still a parse error there and is no longer sent. Databend stays query-only: the Explain panel renders its own text plan, and the overview, health and session panels stay unavailable because Databend has no SHOW STATUS statement at all. Vitess and OceanBase are unchanged; both accept EXPLAIN FORMAT=JSON. D30 and X14 are deleted from docs/BACKLOG.md with their index counts corrected, and no reference to either id remains in the repository.
The Phase C sweep left three sentences in the registry that contradicted what the 2026-08-24 and 2026-09-06 measurements recorded elsewhere. StarRocks: the caveat said the whole monitoring dashboard was unavailable. Since 2026-08-24 each panel is read independently and getMonitoringData() returns overview, performance, slowQueries, tables, indexes and storage, with only errors.activeSessions carrying the engine's sentence. The caveat now names the health request and the active-session panel, which are the two surfaces the missing information_schema.PROCESSLIST actually costs. SingleStore: the caveat still claimed in the present tense that Test Connection, health, the overview and the monitoring dashboard all fail on the prepared-statement message. Those four failures were ours and were fixed on 2026-08-24 when every parameterless statement moved to MySQL's text protocol; the sentence is now past tense and names that fix, keeping the ten-of-fifteen count from the first probe. Its companion sentence said no version is displayed anywhere, which followed from the overview being unavailable; the overview renders, so both the registry and the docs/providers/README.md row now record the version it shows, MySQL 5.7.32, the wire version SingleStore advertises. The registry docblock pointed both #573 and #574 at mysql.md section 5.5. Section 5.5 carries the EXPLAIN grammar probe, section 8 the SHOW STATUS reads, so the pointer names both. The e2e comment above the Databend assertion cited Doris as the precedent for asserting a tier suffix; Doris is full since 2026-09-06 and its suffix is now asserted absent, so the comment cites StarRocks instead.
… registry and the Docker Hub listing Review findings on the Phase C sweep. The SingleStore README row opened in the present tense with a count from its first probe; it now says every surface answers since 2026-09-06 and why the row stays Partial (zeros, not measurements). Two 2026-08-24 StarRocks sentences that the 2026-09-06 Overview re-measurement supersedes now say so. The StarRocks registry caveat claimed the rest of the dashboard renders, which was a provider-level reading, and now says exactly that. The Doris registry test comment counted StarRocks at eleven surfaces; it now names the two it still loses. DOCKERHUB.md carried the pre-fix tier table: Doris is Full, StarRocks's overview renders, and SingleStore's Explain shows the engine's own plan.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
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.



Fixes #573
Fixes #574
What changed
Two statement forms the MySQL provider sent were MySQL grammar that several MySQL-wire relatives reject. Both are ours, both are fixed, and both fixes were measured against live containers of every relative the registry lists.
#573, Overview and Health on Doris.
getHealth(),getOverview()andgetPerformanceMetrics()sentSHOW STATUS LIKE '<name>', which Doris rejects as a parse error. Each now sends one bareSHOW STATUSand picks the row client side, case-insensitively, so the filter that was ours no longer costs the panel. The fabricated fallbacks went with it: an absentThreads_connectedrow omitsactiveConnectionsinstead of sending 0, an absentUptimereportsN/Aand nostartTime, and an absentmax_connectionsreports 0 (the type's own "no limit published") instead of the compiled-in 151.SHOW VARIABLES LIKE 'max_connections'stays, because every engine measured accepts it.#574, Explain on Doris, StarRocks, TiDB, SingleStore.
explainFormat: "mysql-json"was a per-provider constant, so the Explain panel always sentEXPLAIN FORMAT=JSON. The provider now measures its EXPLAIN grammar atconnect(), on the connection the pool check already holds:EXPLAIN FORMAT=JSON SELECT 1, and only if that is refused,EXPLAIN SELECT 1. The first accepted statement is the capability (mysql-json, the newmysql-text, or none). The probe reads success or failure, never the errno, because the relatives share none (Doris and TiDB answer 1105, StarRocks and SingleStore 1064). Beforeconnect()the provider still answersmysql-json, so the type-drivenprovider-metaroute sees no change.Because
POST /api/db/provider-metanever connects (#457), a runtime capability cannot reach the browser through it. The EXPLAIN statement is therefore built on the server:POST /api/db/querygainsexplain: { mode }, resolves the connected provider's strategy, runs only the statement it builds (never the caller's SQL), and namesexplainFormatin the response; the client sends the original statement and stores the plan under the format the response names. Boundparamstravel with an explain request and bind to the built statement, which keeps PR #304's pre-warmed plan for generated statements.mysql-textrenders the engine's own plainEXPLAINoutput as a tree by indentation (TiDB's box glyphs, Doris and StarRocks's indented fragments), withestRowsas the only metric when a column of that name carries a number, and the plain text verbatim in the raw tab.Measured in a real browser (Chrome 151 via Playwright, 2026-09-06), before (
a193debb) and after/api/db/healthWire-level measurements per engine (mysql2 3.24.2, text protocol) are in
docs/providers/mysql.mdsection 5.5. Vitess and OceanBase acceptEXPLAIN FORMAT=JSON, so nothing changes for them; Databend has noSHOW STATUSstatement at all, so its Overview stays unavailable as its row already said, while its Explain panel now renders its own text plan. StarRocks's health still fails on the missinginformation_schema.PROCESSLIST, the engine's own.Registry and docs
Apache Doris moves from
partialtofull: its row said the tier existed for D30 alone, and every surface answers now. StarRocks, SingleStore and Databend keep their tiers with their Explain and overview sentences rewritten to what renders. BACKLOG D30 and X14 are closed and removed.Verification
Gates run locally on the branch: format, lint, typecheck, knip, chart:check, channels:showcase:check, readme:check, security:check, test, build:lib, attw, test:coverage and coverage:check (100.00%).
Note for #582 (mysql.md named citations): this branch adds
statusValue(),probeExplainFormat()and section 5.5 tomysql.tsandmysql.md, so the line numbers that PR re-derives should be taken frommainafter this merges. The existing[mysql.ts:<n>]links were left untouched here for that reason.