Repository navigation
fix(monitoring): one refused read cost the whole dashboard, and three more - #477
Merged
Merged
Conversation
… more
BACKLOG D22, D19, D20, D21 close. Every claim below was measured against a live
engine and verified in Chrome on a production build.
D22 - getMonitoringData composed its seven reads with Promise.all, so one
rejection threw away every panel that had answered. Measured on StarRocks 3.3,
which has no information_schema.PROCESSLIST: the provider answered overview,
performance, slowQueries, tables, indexes and storage, and the user saw nothing
but the engine's sentence about the one read that failed. The four core panels
are now optional and each rejection is recorded under MonitoringData.errors with
the ENGINE's own sentence, which the panel renders in place of its content. It
still throws when all four core reads reject - a dead connection - and rethrows
the first rejection verbatim when they all say the same thing, so
/api/db/monitoring keeps the status code it answered before. Browser-verified:
StarRocks now renders six panels, Tables reads 2 tables / 5 rows, and only
Sessions carries "Unknown table 'information_schema.PROCESSLIST'".
Two fabricated zeros found in the browser while verifying it, both the class D17
removed from the connection count: Quick Stats read "Active 0 / Idle 0" and the
session summary read four zeros with a "Sessions (0)" title for a read the engine
had refused. Both now read N/A, and an answered empty list still reads 0 -
verified side by side, StarRocks N/A against PostgreSQL 18's real 4 / 2.
D19 - Oracle INTERVAL YEAR TO MONTH and INTERVAL DAY TO SECOND arrived as
oracledb objects ({"months":7,"years":3}) that no consumer could read and no
export could replay. They are now normalised at the driver boundary to the
literal Oracle itself accepts back, following the CQL Duration precedent:
'+03-07' and '+05 06:07:08.9', proven by exporting and replaying into the live
engine and comparing server-side. TIMESTAMP WITH TIME ZONE keeps its Date: the
offset is gone before any code here runs (fetchAsString refuses the type,
NJS-119/NJS-021), so docs/providers/oracle.md says so plainly and names the
TO_CHAR that recovers it. No other date path changed.
D20 - oracledb ships no types and there is no @types/oracledb in this tree, so
the blanket `declare module "oracledb" { const oracledb: any }` is replaced by a
declaration of exactly the surface this repo uses. The provider's local
stand-ins for Result and FetchTypeHandler are gone.
D21 - the Cassandra degradation keyed on the WORDING of a refusal. It now keys on
a structural fact asked once per connection. The entry's own premise was refuted
by measurement: system_views is not listed in system_schema.keyspaces on either
engine, so keying there would have emptied all five panels on Cassandra 5.0.9.
The catalog that carries the fact is system_virtual_schema.keyspaces (1.5 ms on
5.0.9; absent on ScyllaDB). The four measured spellings stay as a regression pin.
Browser-verified: Cassandra answers 6 sessions, ScyllaDB degrades without error.
Recorded rather than fixed: D23 (every Oracle DATE and TIMESTAMP cell exports as
a literal Oracle refuses with ORA-01843) and D24 (the Cassandra degradation still
answers an empty panel where absence now has a shape).
|
cevheri
added a commit
that referenced
this pull request
Aug 24, 2026
…to it #465 landed as one commit (0c89963), so main now carries its content with no ancestry in common with the ~90 commits this branch has it through. Git cannot see the two as the same work, which made this merge report more than it should and — the part worth recording — report LESS than it should in one place. NINE TEXT CONFLICTS, all the same shape: main's side is #465 as squashed, ours is that plus the review changes. #477 (the other commit this merge brings) touches none of the nine, so ours is the resolution everywhere, and the result was checked against `main` file by file afterwards to confirm the delta is exactly the reviewed changes and nothing of main's was dropped. AND TEN FILES CAME BACK FROM THE DEAD, WITHOUT CONFLICTING. The per-model modules this PR replaced with a document — `models/qwen3-8b.ts` and its nine siblings — are absent on our side and, against a merge base that predates them, ADDED on main's. Git resolves "added on one side only" by taking the addition, so a merge that reported nine conflicts and no problem had quietly restored the directory the refactor exists to remove. `typecheck` caught it, and only by luck: the restored modules assign `notices`, a field the refactor deleted from `AgentModelProfile`, so they stopped compiling. Had the refactor been purely subtractive they would have compiled, passed every test, and shipped as dead code nobody wrote. The check that finds this class directly is `git diff --name-status HEAD --diff-filter=A` — what the merge ADDS relative to where you were. Here it listed eleven files: ten resurrections and one genuinely new (`PanelUnavailable.tsx`, #477). Anyone merging a squashed base under a stacked branch should run it before trusting a clean merge. Verified on the result: format, lint (0 errors), typecheck, knip, `bun run test` (33/33 groups), build, chart:check (0.1.48 / 0.13.3), and coverage:check at 42537/42537 lines.
cevheri
added a commit
that referenced
this pull request
Aug 25, 2026
…489) D24 - a panel a build cannot answer is now ABSENT with the engine's own sentence rather than empty, on the Cassandra family, on Trino and on the embedded LibreDB. The mechanism landed in #477; this is the audit it asked for. Cassandra's three permanently unanswerable panels (table, index and storage statistics) and the two that need system_views carry their reason; Trino separates a refused table list and a catalog whose connector publishes no statistics from a catalog that genuinely holds no table; LibreDB reports real per-namespace key counts where it used to answer [] on a database with tables, and refuses sessions and indexes with what the engine actually is. Measured against Cassandra 5.0.9, ScyllaDB 2026.2.4 and Trino 476, and verified in Chrome on a production build. The Tables tab's Size card summed a required-field placeholder into "0 B" over a database with bytes on disk, which the LibreDB rows exposed. It now gates the way StorageTab has since #469, and a table with no size of its own reads "-" rather than an empty cell. D23 - an Oracle DATE or TIMESTAMP cell exported as an ISO string Oracle refuses (ORA-01843 / ORA-01861), so a DDL+INSERT export of any ordinary Oracle table could not be replayed. The export now writes TO_DATE / TO_TIMESTAMP / FROM_TZ chosen by the declared type, proven by replaying into live Oracle Free 23ai and comparing by the server. D16 - a pasted connection string's TLS parameter reached the form as "disable", so postgresql://...?sslmode=verify-full said plaintext. The documented spellings now map (postgres sslmode/ssl, MySQL ssl-mode/ssl/useSSL, ADO.NET Encrypt/TrustServerCertificate). The opportunistic values are not guessed onto either end - measured, prefer to require refuses a connection that works and PREFERRED to disable downgrades one that is encrypted - and the refusal is visible, not silent. DOC3 - four marketplace listings still advertised NL2SQL, and two more carried the old engine count. The copy now matches the product, and tests/unit/marketplace-copy.test.ts binds each claim to the code: AI explanation is offered where the engine returns a plan, not on every connection, and "read-only" replaces "never executes what it recommends", which the consented hand-over does. Recorded rather than fixed: D25 (Couchbase turns an RBAC denial into a zero, unmeasurable here with no live cluster), D26 (?ssl=true maps onto the one mode that does not verify) and U19 (a caution rendered as a green success tick).
This was referenced Aug 26, 2026
cevheri
added a commit
that referenced
this pull request
Aug 27, 2026
…ence, and four more (#512) * fix(db,agent,security,docs): the MySQL health panel fabricated an absence, and four more D32 - MySQL's health slow-query read asked for `LEFT(sql_text, 100)` from `performance_schema.events_statements_summary_by_digest`, a column that table does not have, and a bare catch turned the resulting `ERROR 1054` into the row `{ query: "Performance schema not available", calls: 0, avgTime: "N/A" }`. Measured on four live servers (MySQL 26.7.0 on and off, Percona 8.4.11-11, MariaDB 12.3.2): the sentence appeared on every one of them, including the two where `@@performance_schema` is 1 and the Queries panel beside it returned real rows. The health read now uses the statement the panel uses, one mapper feeds both, and an unreadable source empties the line instead of describing itself - a fabricated absence is exactly what the absence rule (#477) exists to prevent. `getSlowQueries()` no longer swallows either, so a denied grant (1142) or an absent tenant schema (1049) reaches the Queries panel as the server's own sentence. AU3 - `Cross-Origin-Opener-Policy: same-origin` is delivered from `securityHeaders()`, and `Cross-Origin-Resource-Policy` is refused with the reason recorded beside the header rule: CORP acts only on a subresource, the only delivery path that reaches a subresource is `next.config.ts`'s build-time `headers()`, and CORP's correct value is the one in the set that depends on deployment topology. What that refusal gives up is now tracked as AU4. `next.config.ts` derives its baked set by probing `securityHeaders()` rather than restating a list of names, so a header that varies with an option cannot be baked by accident. B76 - `declined: "repointed"` is split out of the collapsed `"unavailable"` code, and the five that stay collapsed each carry the reason they have no distinct remedy. The first draft of the rail's sentence claimed the decline persists until the connection is pointed back; it does not - the route writes the new identity onto the run it opens, so exactly one question is declined. Both halves of the corrected sentence are pinned, and a route test pins the behaviour. B12 - a statement that failed at the database records the span the tracker charged it, so the rail's database-time meter no longer counts completed reads only. The charge is a delta, pinned by a two-execution harness run; a ledger entry written before this change folds as a statement with no duration rather than a measured zero. B38 - a run whose workflow sends statements is refused on an engine with no read-only execution path at the point it is opened: 400 with the posture's own sentence, before a run id, a capability probe or a model turn is spent. The `operations` workflow still opens on the same connection, because it sends no statement at all. The drift guard behind the refusal now measures every provider in the `DatabaseType` union rather than restating the list. Also restores two backlog entries #510 overwrote when it branched before #509 merged, renumbered D34/D35, and repoints the citation that named the wrong entry. Verified in Chrome against the built app: COOP present on the document and absent on `/logo.svg` and every `/monaco/vs/*` response, editor, five Monaco workers, a query and the ERD export all unaffected; the B38 refusal answering 400 with no run opened, and an `operations` run succeeding on the same connection; the fabricated MySQL sentence gone from the monitoring panel. B79 and B80 record what that drive found and could not reach. * docs(comments): cite the change that landed, not the backlog entry it closed Deleting an entry when the work lands is this repo's convention, and T4 records what it costs: nothing checks a citation from a source comment, so every comment naming a deleted id goes silently wrong. Closing D32, B12, B38, B76 and AU3 would have left fifty-odd of them across `src/` and `tests/`. Each one was read rather than substituted. Where the sentence explains why the code is the way it is, it now cites #512, the change that made it so. Where the id was the NAME of a defect class rather than a pointer - "a marker keyed on the throw would be emitted for something other than off-ness, D32 one level up" - the class is named instead, because a PR number does not carry that meaning. Where a comment described the defect in the present tense although the same round had fixed it, the sentence was wrong and not just its id: `digestTableMockExecute`'s docblock claimed the shared fixture still invents digest rows. And where what survives is a residual rather than the closed entry, the citation moved to the entry that still exists - T6 for the shared fixture nothing pins, B78 for the delta that rests on one frozen concurrency constant. `grep -rnE '\b(D32|B12|B38|B76|AU3)\b' src tests next.config.ts e2e scripts` is now empty. Comment-only: `bun run test` 13062 pass / 0 fail, typecheck and format clean. * docs(api,agent,tests): the new refusal was missing from its own contract, and a ledger claim had gone stale Two external reviews of #512 were checked finding by finding. What held: - `docs/API_DOCS.md` documented `POST /api/agent/runs` refusals without the 400 this branch added, so the contract still promised `202` for `investigation` on MySQL followed by an `engine-unsupported` run. The refusal is now listed with the message it actually sends, and a note says what changed for a client that read the old shape. - `docs/AGENT.md` said in its opening claims that any other engine "fails profiled acquisition and the run ends `engine-unsupported`", which now describes only the fallback: the route refuses before a run id exists. Both gates are named in the order they run, and the endpoint table row that omitted the 400 carries it too. - Four citations named ids this branch deleted. `docs/AGENT_GUIDE.md` was the worst of them, because the sentence itself had gone stale: it still told the reader a failed statement records no duration, which is exactly what stopped being true. It now says what a failed statement records and what an older refusal does not. `docs/providers/README.md` (two rows) and the provenance prose in `docs/BACKLOG.md` name the change instead of the closed id; the one surviving mention says outright that the id was reused, which is why citing it is unsafe. What did not hold, and was refused rather than adopted: - A finding placed `elapsedMs: 0` in a file that does not exist. The line number was right for `src/components/agent/timeline.ts`, and the observation under it was real: the fold asks `=== undefined`, the two tests around it separate `undefined` from `30`, and a truthiness rewrite passes both while moving a statement the tracker charged 0 ms into the count of statements that measured nothing. A test now pins the measured zero, and reverting the comparison fails that test alone. - SonarCloud's three `act()` reports are dated before this branch and their rule is wrong here: `fireEvent` inside `await act` is what flushes the pending start, not a redundant wrapper. The `toHaveLength` nit on this branch's own test is taken. - The nested-ternary smell in `AgentRail.tsx` is declined, and the reason is the finding underneath it. Three of the four decline sentences are asserted; `connectionDropped` appears in no test in the repository, and all four arms being one expression is why 100% line coverage cannot see that. Extracting the selection would fail the coverage gate until the fourth arm is driven, which is the honest order. Filed as T7. - B77 gains the half a review pointed at: an unreadable digest source leaves the health list empty on purpose, and the projection then hands the model `slowQueryCount: 0` — the absence rule one level up from the row #512 removed. Gates: format, lint, typecheck, knip clean; 13063 pass / 0 fail; build exit 0; coverage 43652/43652 lines (100.00%).
cevheri
added a commit
that referenced
this pull request
Aug 27, 2026
Five backlog entries close. Every one is the same defect wearing different clothes: a reading that could not be taken, or a list that was cut, reaching a reader as a confident figure. The absence rule (#477) is house law here, and these are the places it was not applied. D40 - the Overview panel's connection count. `DatabaseOverview.activeConnections` is optional for the reason its docblock gives, and SQL Server, Oracle and MongoDB all initialised a local to 0 and swallowed the read's failure into it. So a denied VIEW SERVER PERFORMANCE STATE, an unprivileged V$SESSION or a serverStatus without a connections section drew a confident "0 connections" on a server with plenty. All three now omit the key, and a real zero still reads as zero - both arms pinned per provider. The existing SQL Server test was asserting the fabrication rather than a measurement: `expect(typeof overview.activeConnections).toBe("number")` passed because the fabricated 0 is a number. Its purpose-built fixture was dead code, shadowed by a generic branch that matched the same statement, so the assertion had never seen the value it was written for. D41 - two surfaces reading a cap as a count. The Queries tab's *Queries* card summed the calls of a list every provider caps at ten and labelled it the database's query count; *Slow* was bounded by ten however many slow statements the server held. Measured against a MySQL server holding 59 digests for one schema, both cards were the ceiling wearing a measurement's label. On the provider side TRINO_MAX_STATS_TABLES cut the table-stats read inside the provider, where no marker above it could see the cut; it now refuses rather than truncating, and its refusal names the real count. D36 - the migration generator emitted `ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY` for SQLite, which has no ALTER that adds a constraint, so the file was a syntax error wherever it was run. There were two emission sites, not the one the entry described: the ALTER path and, undocumented, `generateCreateTable`, which writes a new table's foreign keys as a separate ALTER. libSQL had the same defect there, so its earlier fix was incomplete. The two sites want different answers - in the CREATE path the key belongs inside the CREATE TABLE, where SQLite accepts it and nothing is lost; in the ALTER path it cannot be expressed and is declined with a comment naming table recreation. D38 - `getConnectionInfo` masked `:secret@` in an authority and nothing else, so a libSQL connection string carrying its whole token as `?authToken=` was returned in the clear. The mask now covers the authority and the value of every credential-shaped parameter, in any position. Two rounds of review found two more shapes in the fix itself: a credential LEADING an ADO-style string was returned verbatim, and narrowing the authority pattern had stopped masking `postgres://user:p/w@host/db`, which the old one masked. What that second one cannot do is covered rather than hidden. `postgres://host:5432/tenant@acme` has the identical shape and carries no secret, so masking one masks the other, and drawing `***` over a port asserts a credential the string never carried - a fabricated reading, which this rule forbids more strongly than it forbids the miss. The docblock says so and the test file pins the gap, so it is visible rather than assumed closed. D42 carries the parse-based fix. U23 - the Storage tab labelled the remainder of its breakdown "Other (TOAST, FSM)" on every engine. TOAST and the free space map are PostgreSQL structures; SQLite, libSQL, MySQL, Oracle, SQL Server, ClickHouse and the HTTP engines have neither. On libSQL it read 4.00 KB under that label against a real 64 KB database: the number right, the words another engine's. Two defects found in the same files while fixing those, both pre-existing: - The Storage tab was the third reader of `errors.tables` and the only one that did not read it. A refused table read arrived as an empty list, `every()` was vacuously true, and the tab drew a measured 0 B for Tables and Indexes with the remainder absorbing the whole database at 100%. It now carries the engine's own sentence, the way its two sibling panels already do, while a genuinely empty database still reads 0 B. - Quick Stats published three cap-bounded figures as counts: "Slow Queries" off a list capped at 10, and "Active" and "Idle" off one list capped at 50, which the two badges split while reading as the server's totals. All three now name the rows they measure. MongoDB's `getOverview` had a second fabricated zero beside the first: `databaseSizeBytes` is optional for the same stated reason, and the Storage tab keys its entire breakdown off whether that key is present. So a denied serverStatus did not hide a number, it replaced an honest refusal with a breakdown over a zero-byte database - and with the table read still answering, the remainder went negative, which `formatBytes` renders as the literal string "NaN undefined". The success path had the same shape through `|| 0`, and so did `getHealth`, whose `databaseSize` string is the one the agent forwards to the model verbatim. Provider docs now cite code by name rather than by line, enforced by a new guard. A line number hand-copied into prose has nothing measuring it. The eight seam rows in both search docs were stale at birth: they entered in one commit as 751/811/831/844/860/873/884/898 while that same commit's declarations sat at 808/868/888/901/917/930/941/955 - a uniform +57, which is exactly why nobody noticed. The rows stayed in ascending order and read as a consistent, plausible list. Today the offset is +65. The guard bans the form, so a correct line number fails it too, and its scope statement names what is not measured yet rather than implying coverage.
cevheri
added a commit
that referenced
this pull request
Aug 27, 2026
* fix(db,ui,docs): a refusal drawn as a measurement, in five places Five backlog entries close. Every one is the same defect wearing different clothes: a reading that could not be taken, or a list that was cut, reaching a reader as a confident figure. The absence rule (#477) is house law here, and these are the places it was not applied. D40 - the Overview panel's connection count. `DatabaseOverview.activeConnections` is optional for the reason its docblock gives, and SQL Server, Oracle and MongoDB all initialised a local to 0 and swallowed the read's failure into it. So a denied VIEW SERVER PERFORMANCE STATE, an unprivileged V$SESSION or a serverStatus without a connections section drew a confident "0 connections" on a server with plenty. All three now omit the key, and a real zero still reads as zero - both arms pinned per provider. The existing SQL Server test was asserting the fabrication rather than a measurement: `expect(typeof overview.activeConnections).toBe("number")` passed because the fabricated 0 is a number. Its purpose-built fixture was dead code, shadowed by a generic branch that matched the same statement, so the assertion had never seen the value it was written for. D41 - two surfaces reading a cap as a count. The Queries tab's *Queries* card summed the calls of a list every provider caps at ten and labelled it the database's query count; *Slow* was bounded by ten however many slow statements the server held. Measured against a MySQL server holding 59 digests for one schema, both cards were the ceiling wearing a measurement's label. On the provider side TRINO_MAX_STATS_TABLES cut the table-stats read inside the provider, where no marker above it could see the cut; it now refuses rather than truncating, and its refusal names the real count. D36 - the migration generator emitted `ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY` for SQLite, which has no ALTER that adds a constraint, so the file was a syntax error wherever it was run. There were two emission sites, not the one the entry described: the ALTER path and, undocumented, `generateCreateTable`, which writes a new table's foreign keys as a separate ALTER. libSQL had the same defect there, so its earlier fix was incomplete. The two sites want different answers - in the CREATE path the key belongs inside the CREATE TABLE, where SQLite accepts it and nothing is lost; in the ALTER path it cannot be expressed and is declined with a comment naming table recreation. D38 - `getConnectionInfo` masked `:secret@` in an authority and nothing else, so a libSQL connection string carrying its whole token as `?authToken=` was returned in the clear. The mask now covers the authority and the value of every credential-shaped parameter, in any position. Two rounds of review found two more shapes in the fix itself: a credential LEADING an ADO-style string was returned verbatim, and narrowing the authority pattern had stopped masking `postgres://user:p/w@host/db`, which the old one masked. What that second one cannot do is covered rather than hidden. `postgres://host:5432/tenant@acme` has the identical shape and carries no secret, so masking one masks the other, and drawing `***` over a port asserts a credential the string never carried - a fabricated reading, which this rule forbids more strongly than it forbids the miss. The docblock says so and the test file pins the gap, so it is visible rather than assumed closed. D42 carries the parse-based fix. U23 - the Storage tab labelled the remainder of its breakdown "Other (TOAST, FSM)" on every engine. TOAST and the free space map are PostgreSQL structures; SQLite, libSQL, MySQL, Oracle, SQL Server, ClickHouse and the HTTP engines have neither. On libSQL it read 4.00 KB under that label against a real 64 KB database: the number right, the words another engine's. Two defects found in the same files while fixing those, both pre-existing: - The Storage tab was the third reader of `errors.tables` and the only one that did not read it. A refused table read arrived as an empty list, `every()` was vacuously true, and the tab drew a measured 0 B for Tables and Indexes with the remainder absorbing the whole database at 100%. It now carries the engine's own sentence, the way its two sibling panels already do, while a genuinely empty database still reads 0 B. - Quick Stats published three cap-bounded figures as counts: "Slow Queries" off a list capped at 10, and "Active" and "Idle" off one list capped at 50, which the two badges split while reading as the server's totals. All three now name the rows they measure. MongoDB's `getOverview` had a second fabricated zero beside the first: `databaseSizeBytes` is optional for the same stated reason, and the Storage tab keys its entire breakdown off whether that key is present. So a denied serverStatus did not hide a number, it replaced an honest refusal with a breakdown over a zero-byte database - and with the table read still answering, the remainder went negative, which `formatBytes` renders as the literal string "NaN undefined". The success path had the same shape through `|| 0`, and so did `getHealth`, whose `databaseSize` string is the one the agent forwards to the model verbatim. Provider docs now cite code by name rather than by line, enforced by a new guard. A line number hand-copied into prose has nothing measuring it. The eight seam rows in both search docs were stale at birth: they entered in one commit as 751/811/831/844/860/873/884/898 while that same commit's declarations sat at 808/868/888/901/917/930/941/955 - a uniform +57, which is exactly why nobody noticed. The rows stayed in ascending order and read as a consistent, plausible list. Today the offset is +65. The guard bans the form, so a correct line number fails it too, and its scope statement names what is not measured yet rather than implying coverage. * docs(backlog): five entries close, seven open in their place Deleted: D36 (the migration generator's invalid SQLite DDL), D38 (the connection-string mask), D40 (the Overview panel's fabricated connection zero), D41 (a cap read as a count), U23 (PostgreSQL storage vocabulary on every engine). The three section index lines are recomputed from the surviving headings rather than edited by hand, which the structure guard added last round now checks in both directions. The seven that open were all measured while closing those five, and each one is here rather than in the PR for a stated reason: D42 - the mask cannot cover an authority password holding a character RFC 3986 reserves, because `postgres://host:5432/tenant@acme` has the identical shape and holds no secret. The fix is parsing, not a better pattern. D43 - SQLite gained `ALTER COLUMN ... SET/DROP NOT NULL` in 3.53.0, measured on 3.53.0: it rewrites the stored schema and is enforced on insert. The generator declines it on both ids. It cannot simply stop: sqld ships 3.47.0, and the `sqlite` provider runs on whichever SQLite its runtime bundles, so a migration file handed to a human would run on one deployment and fail on another. D44 - `databaseSizeBytes` is fabricated as 0 wherever the size is unknown, in 14 of 16 type-ids, and the Storage tab keys its whole breakdown off whether the key is present. Cassandra's own comment is the precedent and states the argument. 14 provider files, 14 docs, 14 test files, which is why it is an entry. D45 - on SQL Server 2019 and earlier the connection count is under-reported rather than refused. Microsoft documents `sys.dm_exec_sessions` as row-filtered, not refused, and `sys.configurations` as needing only `public` below 2022, so the statement SUCCEEDS and returns the caller's own session. Distinguishing that needs a permission probe and a live instance with an ungranted login. D46 - one fabricated connection zero survives the sweep, in the shared search provider, and three of the five comments citing the old encoding are now false while two remain true: for `maxConnections` zero and absence really are one fact, which is what Druid and Trino cite. The three false ones are corrected here; moving the encoding is the entry. U24 - two absences the Storage tab still renders as measurements: a measured zero total draws the remainder bar full, and the same zero puts the literal string "NaN undefined" in the cell beside it, because `formatBytes` takes `Math.log` of a negative. A refused overview also drops the engine's sentence in a file that carries it for two other panels. DOC4 - 62 line citations remain in the provider docs and every checkable one is stale: of 69 machine-checkable before this round, 68 were wrong; after 18 fixes, 62 remain and all 62 are wrong, over 12 docs. The other 209 point at expressions and table rows, so no heuristic can judge them and no figure is claimed for them. * docs(src,tests,docs): the five closed entries left 19 dangling citations Deleting an entry leaves every bare id that cited it pointing at nothing. This is the third round in a row where that happened, and T4 in the backlog is the standing record of the class, so the repointing is part of the same PR rather than a follow-up. Nineteen citations across 13 files now name #515, the PR that closed the work, which a reader can actually open. They were spread across code comments (the migration generator, the Trino introspection pair, three monitoring tabs), one provider doc and seven test files. One is deliberately left alone. `tests/unit/backlog-structure.test.ts` says "main carried D36, D37 and D38 twice", which is a statement about what happened in #511 and #513, not a citation of an entry. It is still true, and D37 is still an open entry, so repointing it would have made a correct sentence wrong - the same trap as blanket-repointing the five comments about the "0 means not published" encoding, two of which describe `maxConnections` and remain correct.
cevheri
added a commit
that referenced
this pull request
Sep 6, 2026
…connect (#573, #574) (#599) * fix(mysql): read SHOW STATUS without LIKE and report absence 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. * docs(mysql): move the SHOW STATUS portability note below the method table 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. * feat(explain): add the mysql-text strategy for plain EXPLAIN output 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. * feat(explain): build the EXPLAIN statement on the server (#574) 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. * style(explain): drop an em-dash from the explain refusal comment * fix(explain): bind an explain request's parameters to the built statement 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. * fix(explain): treat a blank estRows cell as absent and refuse an unregistered 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. * feat(mysql): measure the EXPLAIN grammar at connect and declare it as 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. * docs(providers): close D30 and X14, re-tier Doris, and record the 2026-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. * docs(providers): correct three stale registry caveats found in review 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. * docs(providers): finish the re-measurement wording in the README, the 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.
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.



Four BACKLOG entries close. Every claim here was measured against a live engine and then verified in Chrome on a production build, not through the provider alone.
D22 — one refused read discarded the whole monitoring dashboard
BaseDatabaseProvider.getMonitoringDatacomposed its seven reads withPromise.all, so the first rejection threw away every panel that had already answered.Measured on StarRocks 3.3 (
3.3.22-753696f), which has noinformation_schema.PROCESSLIST:The four core panels are now optional, and each rejection is recorded under
MonitoringData.errorscarrying the engine's own sentence — the only text that says what the database actually refused. A panel that was not requested (includeTables: false) stays absent with no error entry, so "not asked for" remains distinguishable from "asked and refused".It still throws when all four core reads reject, which is what a dead connection looks like, so
/api/db/monitoringkeeps its error path untouched. When all four say the same thing the first rejection is rethrown verbatim, so anAuthenticationErroris not re-classified into a generic one by its text and the route answers the status code it answered before.Browser-verified on StarRocks: the Overview, Performance, Queries, Tables, Storage and Pool tabs all render — Tables reads
2 tables / 5 rowswith real sizes — and only Sessions carries "This database could not answer this panel. Getting analyzing error. Detail message: Unknown table 'information_schema.PROCESSLIST'."Two fabricated zeros found in the browser while verifying it, both the class removed from the connection count on 2026-08-24:
Active 0 / Idle 0for anactiveSessionsread the engine had refused.Sessions (0)title, above the panel that said the engine could not answer.Both now read
N/A, and the same for the admin Operations card titles. An answered empty list still reads0, because zero sessions is a measurement — verified side by side: StarRocksN/A, PostgreSQL 18 a real4 / 2, andSlow Queries 0on StarRocks because that read genuinely answered[].D19 — Oracle intervals arrived as objects nothing could read
INTERVAL YEAR TO MONTHcame back as{"months":7,"years":3}andINTERVAL DAY TO SECONDas{"fseconds":900000000,"seconds":8,...}. Lossless, and unreadable by every consumer and unreplayable by the export.They are now normalised at the driver boundary to the literal Oracle itself accepts back, following the CQL
Durationprecedent already documented for Cassandra:INTERVAL YEAR TO MONTH{"months":7,"years":3}+03-07INTERVAL DAY TO SECOND{"fseconds":900000000,...}+05 06:07:08.9The signed form and not the keyword form, because that is what the server accepts —
'INTERVAL ''3-7'' YEAR TO MONTH'as a bound value isORA-01867: the interval is invalid, while'+03-07','-03-07','+00-00'and'+123456789-11'are all accepted. Proven end to end: exported through the real export path, replayed into live Oracle Free 23ai, and compared by the server (s.iym = r.iym AND s.ids = r.ids→ EQUAL for every row). Browser-verified in the grid:+03-07and+05 06:07:08.9under column typesINTERVAL YEAR TO MONTH/INTERVAL DAY TO SECOND.TIMESTAMP WITH TIME ZONEkeeps itsDate, and the doc now says why rather than promising a fix. The offset is gone before any code here runs: the driver hands over a bareDate,fetchAsStringrefuses the type (NJS-021) and afetchTypeHandlertoSTRINGrefuses it too (NJS-119). Measured with three stored offsets, the string form is strictly worse — it reports the reader's zone for all three and drops.345.docs/providers/oracle.md§5.5 names theTO_CHAR(col,'YYYY-MM-DD HH24:MI:SS.FF6 TZR')that recovers both the zone and the sub-millisecond digits. No other date path changed —DATE,TIMESTAMPand both zoned forms serialise byte-identically to before.D20 — the Oracle provider was compiled against
anyMeasured:
oracledb6.10.0 ships no.d.tsand there is no@types/oracledbin this tree, so the entry's first branch is impossible. The blanketdeclare module "oracledb" { const oracledb: any }is replaced by a declaration of exactly the surface this repo uses (DbType,Metadata,FetchTypeHandler,Result,IntervalYM/IntervalDS,Connection,Pool, and theDB_TYPE_*members). The provider's local stand-ins forResultand the fetch-type-handler contract are gone, and the test mock's identities are typed. Onlyoracle.tsimports the module, so the tightening is contained.D21 — the Cassandra degradation keyed on the wording of a refusal
It now keys on a structural fact asked once per connection. The entry's own premise was refuted by measurement, which is the main finding here:
A virtual keyspace is not in
system_schemaon either engine, so implementing D21 as written would have emptied all five monitoring panels on the engine that answers them. The catalog that carries the fact issystem_virtual_schema.keyspaces—system_views, system_virtual_schemaon Cassandra 5.0.9 in 1.5 ms, andKeyspace system_virtual_schema does not existon ScyllaDB.readServerFactsnever throws and cannot fail aconnect(): the catalog answering decides by name, a refusal decides by category/code, and anything else (timeout, unreachable) claims no absence so the reads go out exactly as before. A read that needssystem_viewson a build that measurably has none is not sent — three fewer round trips per monitoring refresh, asserted by test. Cost stated in the code and the doc: one extra statement per successful connect.The four measured error spellings stay pinned as a regression test, as the entry asked. A fifth capitalisation turned up while re-measuring (ScyllaDB writes
Keyspace ... does not existwhere Cassandra writeskeyspace ...) — exactly the drift the text match was exposed to.Browser-verified: Cassandra 5.0.9 answers 6 sessions with no failure panel; ScyllaDB 2026.2.4 degrades to an empty list with no error, and its Overview still reads
Connections N/A / not published.Recorded rather than fixed
DATEandTIMESTAMPcell exports as a literal Oracle refuses. Found while verifying the interval replay: the intervals replay, and'2026-08-24T07:11:12.345Z'in the same file isORA-01843: An invalid month was specified. It affects every date column on the engine, and the fix belongs inresult-export.tsrather than the provider.Gates
bun run formatbun run lintbun run typecheckbun run knipbun run buildbuild:lib+attwOne thing worth flagging for review: the six new component test blocks initially failed inside
tests/run-components.sh's group 15 and passed standalone. Cause was scoping, not flake —bun:testregistersafterEachon the enclosingdescribeonly, so the file's existingcleanup()hook did not reach a new sibling block and the control arm queried the previous test's DOM. Each new block now registers its own.