Skip to content

fix(providers): every MySQL DDL and DML reported a failure for work it had done, plus five more - #469

Merged
cevheri merged 2 commits into
mainfrom
chore/backlog-round5
Aug 23, 2026
Merged

cevheri merged 2 commits into
mainfrom
chore/backlog-round5

Conversation

@cevheri

@cevheri cevheri commented Aug 23, 2026

Copy link
Copy Markdown
Member

Round 5 of the BACKLOG sweep. Six defects fixed, each measured against a live server before the change and re-measured in a browser against a production build after it. Four were BACKLOG entries (D7, D12, X7, X8); two were found while closing them, and one of those is the most serious thing in the PR. BACKLOG 106 -> 108 (four removed, six follow-ups recorded).

The one a user would have hit first

Every MySQL DDL and DML statement reported a failure for work it had done

Not a backlog entry - found while filling columnTypes. query() called .map on mysql2's first return value, which is a ResultSetHeader object, not an array, for anything that returns no rows. Measured through the provider against MySQL 26.7.0, with an interleaved SELECT proving the server had already applied each statement:

FAIL DROP TABLE ...       -> TypeError: result.rows.map is not a function
FAIL CREATE TABLE ...     -> TypeError: result.rows.map is not a function
FAIL INSERT ...           -> TypeError: result.rows.map is not a function
OK   SELECT * FROM ...    -> rowCount=1 rows=[{"id":1,"note":"beta"}]      <- the "failed" work landed
FAIL tx INSERT            -> TypeError: result.rows.map is not a function
OK   SELECT * FROM ...    -> rowCount=1 rows=[{"id":2,"note":"gamma"}]     <- committed

This is a SQL IDE. Reporting an error for a statement the server ran is the worst available answer, because the user retries it. Both sites now go through one envelope builder that branches on the shape, reporting affectedRows in rowCount - the same answer postgres (rowCount), mssql (rowsAffected[0]) and sqlite (changes) already give.

Browser, production build, MySQL (r5): CREATE TABLE ui_probe (...) reports "0 rows - 22ms" with the table created (confirmed in information_schema), and INSERT ... VALUES (1,'alpha'),(2,'beta') reports "2 rows - 9ms" with both rows present.

The tests could not see it: they mocked execute as returning an array, and the mock's TYPE made the real shape unexpressible. That type is now wide enough, which is the part that keeps it fixed.

X7 - the DDL export typed a numeric or a timestamp column as TEXT

Nothing was wrong with the inference; it never saw a type. pg, mysql2, oracledb and mssql now fill QueryResult.columnTypes with the engine's own spelling, joining the six families that already did. Every mapping measured against a live engine rather than remembered:

  • Postgres resolves OIDs through a generated pg_type table (149 built-ins, format_type spellings). Verified over all of dvdrental: 128 result columns, 125 named, 0 wrong, 3 absent - the three being one user-defined enum. A catalog round trip is not available at three of the four call sites (query() releases its client first; queryReadOnly() promises exactly one statement inside BEGIN READ ONLY).
  • MySQL separates its shared type codes by charset 63 and the length tiers, not by assumption: 38 of 39 columns match information_schema.DATA_TYPE, and POINT is indistinguishable from GEOMETRY on the wire.
  • Oracle and SQL Server pass their own name through; precision and scale are deliberately not spelled back (Oracle reports scale -127 for 1/3, mssql reports 65535 as a sentinel for varchar(MAX)).

Browser, Postgres (r5), SELECT id, price, dbl, flag, name, ts, tsz, d, j, u, b FROM r5_types:

id bigint · price numeric · dbl double precision · flag boolean · name character varying
ts timestamp without time zone · tsz timestamp with time zone · d date · j jsonb · u uuid · b bytea

and the DDL that same result exports:

CREATE TABLE table_name (
  "id" bigint, "price" numeric, "dbl" double precision, "flag" boolean,
  "name" character varying, "ts" timestamp without time zone, ...  "b" bytea
);

price and ts were TEXT before. The backlog entry's own acceptance test.

The consequence of X7, fixed here rather than left to be found

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 replaying the generated CREATE TABLE into its own engine:

Target Declared type verbatim (before) After
Postgres OK OK, unchanged
MySQL ERROR 1064 ... near ', `body` text,' name text, price double
Oracle ORA-00906: missing left parenthesis NAME VARCHAR2 4000
SQL Server parses, then nvarchar length 1, varbinary length 1, decimal(18,0) - and its own INSERT fails Msg 2628 nvarchar(max), varbinary(max), float

A bare name now completes to the target dialect's unbounded form, and a FOREIGN engine's declaration is re-spelled rather than written through - both shells keep the connection type beside the tab's own result, so exporting an Oracle result as Postgres DDL is reachable, and VARCHAR2 in Postgres DDL replays nowhere. Two dialect corrections came out of the measurements: mssql char -> NVARCHAR(MAX) (char(max) is not legal T-SQL) and oracle NVARCHAR2 -> VARCHAR2(4000) (NVARCHAR2 tops out at 2000).

X8 - a binary value exported as its JSON shape

sqlValue had no binary branch, so a bytea/BLOB cell was written as a quoted {"type":"Buffer","data":[...]}. Replaying that did not fail - it stored 46 bytes of JSON text where six bytes belonged, because bytea's escape input accepts it. The DDL had the same hole (a binary column was recreated as TEXT).

Each dialect now writes a literal its own engine accepts, every spelling measured:

  • Postgres '\x...'::bytea - X'...' there is a bit string (pg_typeof(X'0102') is bit) and cannot be cast to bytea; the plain form survives standard_conforming_strings = off.
  • MySQL, SQLite, Trino X'...' - MySQL has no 0x spelling for an EMPTY value (LENGTH(0x) is ERROR 1054), and an empty cell is not a case an export gets to refuse.
  • SQL Server, Cassandra 0x... - both reject X'...'.
  • Oracle HEXTORAW('...') - it parses neither literal form.
  • ClickHouse unhex('...'); Couchbase is the honest no-binary-type case and gets the \x... text a human can decode.

Replayed into seven engines, six bytes each. Browser: the exported INSERT for the Postgres row is ... '\x0102ab'::bytea);, and replaying the browser-produced file gives hex 0102ab, bytes 3.

D7 - a fabricated cache hit ratio, in eight providers rather than three

The entry named MySQL, MongoDB and LibreDB. The sweep found five more, and all eight are honest now.

  • MySQL: the entry's three sites had already been fixed by fix(mysql): a MariaDB server read as MySQL, and an unmeasured metric read as 99% (#424) #458, so the entry was stale - but getHealth() ran the ratio query uncaught, and a tenant with no performance_schema DATABASE (measured on OceanBase as ERROR 1049) lost the whole health panel rather than one metric.
  • SQLite claimed a fixed 95% ("Reasonable estimate for in-memory cache") derived from PRAGMA cache_size, a configuration value that is -2000 by default and therefore always truthy. Neither bun:sqlite nor node:sqlite exposes sqlite3_db_status, and no PRAGMA stands in, so it is omitted permanently and the doc says why.
  • Couchbase published a 0% when the stats read was denied - and a denial is the ORDINARY case there. DEFAULT_THRESHOLDS rates 0 as critical, so the panel invented a cache fault the cluster never reported.
  • Postgres, Oracle, SQL Server: COALESCE(..., 100) inside our own SQL, || 100, || 0. Reproduced both absence arms live for each: a fresh database with NULL counters, and a login holding only CONNECT / CREATE SESSION (VIEW SERVER PERFORMANCE STATE permission was denied, ORA-00942).

Two of these were pinned by tests - one named "reports zero rather than a perfect score when denied", one commented // Default fallback. A third assertion was unreachable behind a branch that matched first, so the suite was asserting the fabrication.

bufferPoolUsage went with the ratio in three providers, because it was the ratio wearing a second name (Oracle and SQL Server assigned it literally; Postgres computed the same quotient from pg_stat_database), so the Performance tab drew and rated one quantity as two independent gauges. What a real occupancy gauge would cost on each engine is recorded in D14 rather than guessed at.

Browser: MySQL 98.8%, Oracle 99.5% measured; SQLite "Cache Hit N/A - Not measured" where it used to say 95% "Excellent"; Buffer Pool "Not measured" on Oracle and SQLite.

D12 - a pasted rediss:// URL landed on SSL mode disable

It arrives as require now, and so does couchbases:// - whose excuse for being left out of the panel work ("the provider re-reads the scheme there") was simply untrue: CouchbaseHttpTransport picks http vs https from config.ssl alone and was posting plain HTTP to the TLS management port 18091. require rather than a verifying mode, in both cases, because a self-hosted --tls-port Redis and a self-hosted Couchbase node present self-signed certificates; a paste encrypts but never silently claims to have checked a chain.

mongodb+srv:// is the one secure scheme deliberately left unset: the driver receives the URI verbatim and turns TLS on with chain verification, so handing it our require (rejectUnauthorized: false) would have stopped an Atlas certificate being verified. Pinned as a negative test.

Browser, both arms: pasting rediss://localhost:6390 fills the form with SSL / TLS: REQUIRE; pasting redis://localhost:6379 leaves it on the default. End to end against a TLS-only Redis (--port 0 --tls-port 6390, so no plaintext port exists): rediss:// connects and answers PONG in 14ms, redis:// fails with Connection is closed.

Follow-ups recorded, not smuggled in

  • D13 - Oracle answers every non-SELECT with rowCount: 0, including the rows it just wrote (rows.length instead of result.rowsAffected). Found by the same audit; the only one of the five SQL providers that under-reports DML silently.
  • D14 - what a real buffer-pool gauge costs on each engine (pg_buffercache is an extension, sys.dm_os_buffer_descriptors is a full scan), so the field is not refilled by guesswork.
  • D15 - SQLite's table size is rowCount * 100 and the storage panel sums it. Needs a required type to allow absence, and dbstat exists on node:sqlite but not bun:sqlite.
  • D16 - the TLS a connection string carries in its QUERY STRING (?sslmode=, ?ssl-mode=, Encrypt=True) is still dropped. Not the same one-line shape: prefer/allow/PREFERRED have no representation in SSLMode, and mapping them is a security decision. It also corrects a false claim the old entry carried.
  • X9 - what columnTypes still cannot name, with the numbers: the user-defined type, POINT vs GEOMETRY, bit (which needs opposite families on pg and mysql2), and the mssql zero-row path.
  • X10 - MySQL and Cassandra stringify their bytes at the driver boundary, so the new binary literal never reaches them. Measured in the browser: the MySQL grid shows 0x0102ab where Postgres shows \x0102ab, and its INSERT exports the eight-character string. This BOUNDS what X8 closed, which is why it is stated rather than implied.

Verification

Gate Result
bun run format clean, 921 files
bun run lint 0 errors
bun run typecheck clean
bun run knip clean
tests (run-core.sh + components) 11964 pass / 0 fail, all 334 core files
bun run coverage:check 100.00%, 41425/41425 lines
bun run build exit 0
bun run build:lib + attw exit 0, node16/bundler green
gitleaks (pinned digest, eb88fad1..HEAD) 1 commit scanned, no leaks

Provider tri-sync held for all nine providers touched: code, docs/providers/<id>.md and tests/integration/db/<id>-provider.test.ts move together for postgres, mysql, oracle, mssql, sqlite, mongodb, couchbase, libredb and redis.

Live engines used: postgres:18, MySQL 26.7.0, SQL Server 2022 CU26, Oracle Free 23ai, MongoDB, Redis (plus throwaway ClickHouse, Trino and Cassandra containers for the binary replay). One environment note for whoever repeats this: SQL Server would not start until fs.aio-nr had room - the two ScyllaDB containers from the previous round held all 65536 aio contexts, and the failure reads Unable to create a new asynchronous I/O context.

…t 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).
…d 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.
@cevheri

cevheri commented Aug 23, 2026

Copy link
Copy Markdown
Member Author

Checked the external review's findings against the code rather than taking them as read. Every factual claim in it holds:

  • declaredColumnTypes does build through a Map + Object.fromEntries (column-types.ts:48-57), and completeDeclaredType does guard with Object.hasOwn at the line cited (result-export.ts:332, and again at :352).
  • Observation 1 is accurate, including the reason it is harmless: the mssql transaction path takes fields from Object.keys(recordset[0]) while columnTypes comes from recordset.columns, and every consumer resolves its columns from fields (resolveColumns in csv.ts:107-111, declaredTypeOf in ResultsGrid.tsx:92) and only looks columnTypes up by name, so an extra key is inert.
  • Observation 2 is accurate and is what X10 records.

One correction, minor: the OID table is not extracted from pg_type.dat. It is generated by querying a live pg_type (oid < 16384 AND nspname = 'pg_catalog', the reproduction query is in the module comment); pg_type.dat is cited there only as the reason those OIDs are stable.

And one thing the review missed, which is why "correctly deferred" was too generous in one place. The review read the absence of the other dialects from STANDS_ALONE as settled. The module's own comment is what made that reading reasonable, and the comment covered only half the cases: it justified having no row for clickhouse/trino/sqlite/cassandra on the grounds that those engines spell their own types out in full, which is true of the names THEY declare and says nothing about a bare name arriving from somewhere else. Measured just now, one Oracle result under each target:

Target NAME (VARCHAR2) DBL (BINARY_DOUBLE) BODY (CLOB)
postgres TEXT DOUBLE PRECISION TEXT
mysql TEXT DOUBLE TEXT
mssql NVARCHAR(MAX) FLOAT NVARCHAR(MAX)
clickhouse VARCHAR2 BINARY_DOUBLE CLOB
(no connection) VARCHAR2 BINARY_DOUBLE CLOB

The last two rows replay nowhere, and they are reachable exactly the way the measured rows are - both shells pass the ACTIVE connection's type beside the tab's own result, so querying Oracle and switching to a ClickHouse connection before exporting is enough. The no-connection row is a small regression rather than a pre-existing gap: before these four drivers declared anything, a value-shaped guess wrote portable standard SQL there.

Widening the rule is not the fix. Re-spelling from the family whenever the target is unmeasured breaks Trino, whose own bare varchar is legal and unbounded and would become a TEXT Trino does not have. So each remaining dialect needs its own measured row, one live container at a time, the way the first four were done. Recorded as X11 and the misleading half-reason in the comment is corrected in a9acb48 - entry and comment only, no behaviour change.

@cevheri
cevheri merged commit f970801 into main Aug 23, 2026
15 checks passed
@cevheri
cevheri deleted the chore/backlog-round5 branch August 23, 2026 23:22
@sonarqubecloud

Copy link
Copy Markdown

cevheri added a commit that referenced this pull request Aug 23, 2026
…nly the AppImage

The chart version was written when 0.13.3 was one file-mode fix; #469 landed on top
of the bump and its content is what a chart user actually experiences - a MySQL
statement reported as failed while the server applied it, eight providers publishing
a cache hit ratio they never measured, and a pasted rediss:// URL connecting in the
clear.

That last one also moves `containsSecurityUpdates` to true. No CVE and no dependency
moved, but a connection the user asked to encrypt was made in the clear, and this
annotation is what an operator reads when deciding whether to upgrade.

0.1.47 has not published yet (the image gate skipped its publish jobs on the bump
commit), so this is the last moment the annotation can be corrected: #167 freezes it
once the chart is out.
cevheri added a commit that referenced this pull request Aug 24, 2026
…d an Oracle LOB failed the whole query (#472)

* fix(providers): a ScyllaDB connection could not be created at all, and an Oracle LOB failed the whole query

Six defects, each measured against a live server before and after, and each
re-measured in a browser against a production build. Four were BACKLOG entries;
two were found while closing them.

ScyllaDB could not be added to the product. Five monitoring reads query
Cassandra's `system_views` virtual tables, ScyllaDB has no such keyspace, and the
provider degraded a monitoring read on a permission denial and on nothing else -
deliberately, so a typo in our own CQL would not hide behind an empty panel. So
the reads threw, Test Connection with them, and `handleConnect` gates the SAVE on
that same request: the dialog refused to store the connection.

The discriminator the entry left open turned out not to exist structurally.
Measured through cassandra-driver 4.9.0 against both servers: an absent keyspace,
a table typo, a column typo and a keyspace typo all arrive as code 8704 with
`keyspace` and `table` undefined. The server's sentence is all there is, so the
provider now reads the NAME the server refused and degrades only for a keyspace it
knows is optional - `system_views`, nothing else. `system_viewz` still throws, on
the engine that has the keyspace. Measured after: all thirteen surfaces answer on
ScyllaDB 2026.2.4, and the Cassandra 5.0.9 control arm is unchanged and still full.

The save gate is answered rather than special-cased. A server that connects and
runs statements is usable, so `POST /api/db/test-connection` now separates the two
facts: a failed connect is still a failure, a failed health read after a successful
connect is a degraded success carrying the server's own sentence. Establish
Connection reports that sentence and saves nothing on the first click; a second one
saves. Three published engines were unsaveable on that gate - ScyllaDB plus
StarRocks and SingleStore, which fail health for their own reason (BACKLOG D8).
Browser: a ScyllaDB connection is created through the dialog on the first click,
the badge reads Online rather than Slow, and the monitoring dashboard renders
panels instead of one Connection Error page.

An Oracle query touching a CLOB, NCLOB or BLOB failed entirely. Not merely an
unreadable cell: oracledb hands back a Lob stream, `JSON.stringify` on it throws
`cannot serialize cyclic structures`, and `POST /api/db/query` answers with
`NextResponse.json` - so the whole SELECT was a 500. In process the value reached
the grid, the CSV, both SQL exports and the agent's summary as the stream's
internals. A per-call `fetchTypeHandler` maps CLOB and NCLOB to STRING and BLOB to
BUFFER; per call rather than the process-wide `fetchAsString` globals, which would
also apply to `getSchema` and every monitoring read and would leak out of an
embedded host. No size cap, argued from measurement: a 16.4M-character CLOB costs
66ms and 1.5MB, the runtime's own ceiling fails loudly, and a truncated CLOB looks
complete in the grid while the export would write the truncation as though it were
the value.

D13 - Oracle answered every non-SELECT with `rowCount: 0`, including rows it had
just written. `rowsAffected` was on the wire all along. Both call sites now go
through one builder, matching the shape mysql got in #469. Measured: INSERT 1,
INSERT-SELECT 3, UPDATE 4, unmatched DELETE 0, and the transaction path does not
double-count. Browser: an INSERT reports "1 rows".

D15 - SQLite's per-table size was `rowCount * 100` ("Assume 100 bytes average per
row") and the storage panel summed it into the Data figure. Measured against a
1.7MB file: the guess answered 20,000 bytes for both tables where `dbstat` reports
823,296 and 4,096 - 40x under and 5x over. `dbstat` exists on `node:sqlite` and not
on `bun:sqlite`, and `LIBREDB_SQLITE_DRIVER` picks between them, so both answers
have to be right: the fields are optional now and absent under Bun. Browser, the
node driver the server actually runs: Tables 808 KB / Indexes 916 KB / Other 4 KB
against a 1.69 MB database, and the arithmetic closes.

X11 - a foreign engine's type name reached a target dialect nobody had measured. Two
cells of that entry's own table were mis-attributed (ClickHouse takes `VARCHAR2` and
`CLOB`), and the defect was wider: `decimal` narrows to `Decimal(10, 0)` there,
MySQL's `set` becomes `UInt64`, and on SQLite an `enum` column takes NUMERIC affinity
so `'007'` reads back as the integer 7. Four measured `STANDS_ALONE` rows added
(sqlite, clickhouse, trino, cassandra), Trino's own bare `varchar` kept because it is
legal and unbounded there, and the no-dialect case now writes portable standard SQL
rather than an Oracle-only spelling. 8 of 15 dialects measured; the other 7 parse no
CREATE TABLE at all and get the portable form, with the reason recorded per dialect.

X10 - MySQL and Cassandra stringified a Buffer to `0x<hex>` at the driver boundary,
so the per-dialect binary literal never got bytes. Cassandra's stated reason had
expired: it existed because the wire JSON was unreadable, and that JSON is exactly
what the shared binary module reads. Measured before: MySQL stored eight characters
of text into a BLOB, successfully; Cassandra refused the statement outright and lost
the value. After: `X'0102ab'` replays as three bytes on both. The other three rows of
Cassandra's normalisation table were re-measured and left alone, with the reason each
is still valid written down. Browser: the MySQL grid reads `\x0102ab` where it used to
read `0x0102ab` - one spelling everywhere.

Follow-ups recorded rather than smuggled in: D17 (an unmeasured connection count
still reads 0), D18 (Oracle NUMBER and SQL Server DECIMAL hand back a number whose
digits are already gone), D19 (Oracle's timestamps fold their offset and its
intervals arrive unreadable), D20 (oracledb is declared `any`, so nothing in that
provider is checked against the driver), X12 (a declared type the export cannot map
still reaches every target verbatim - `jsonb` replays into neither ClickHouse nor
Trino), X13 (every Cassandra CREATE TABLE the export writes is rejected, because CQL
requires a PRIMARY KEY) and B57 (the agent reads a binary cell as its wire JSON).

* docs(cassandra): the degradation discriminator is a message match, and say what that risks

An external review of #472 flagged the regex in `absentKeyspace()` as load-bearing
text the project does not control. It is, and the consequence is worth writing next
to it rather than leaving a reader to work out: a future build that rephrases the
refusal stops matching and the five monitoring reads throw again. What that costs is
bounded, because the same PR stopped the connection dialog gating its save on the
health read - the connection stays creatable and the failure reaches the user as the
server's own sentence - and the four measured spellings are pinned by tests, so a
rephrase fails a test rather than going quiet.

The review did not name the structural alternative and there is one: asking
`system_schema.keyspaces` whether `system_views` exists at all replaces the text
match with a property of the server, at the cost of one read per connection.
Recorded as BACKLOG D21 with what would and would not change. Comment and entry
only, no behaviour change.
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).
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