From 4fc5c3e792100852a35e9d94c2c7a64cbc0f52a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 16:51:27 +0000 Subject: [PATCH 1/2] test(driver-sql): route the comparand-type cell through the dialect matrix, and promote MATRIXED MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves the two seeded DIALECT_LEDGER rows and turns the census measurement "every dialect-scored cell has at least one matrix-routed suite" into an enforced invariant. - sql-driver-comparand-type-conformance.test.ts now iterates DIALECT_CELLS. Its case-set claims the six accepted comparand types "compile everywhere"; that sentence had only ever been measured on SQLite. Measured on live PostgreSQL 16.13: all eight executed cases answer the case-set's row ids. The door-refusal half stays declared once — parseFilterAST is upstream of every driver and has no dialect in it. - sql-driver-icontains-and-retired-operators.test.ts declares dialectCell('sqlite'). Measured BY OPERATOR before marking: the seven text operators it reaches are exactly the seven FILTER_TEXT_CASES reaches, and sql-driver-text-case-conformance.test.ts runs that table once per cell. - check-driver-conformance.mjs gains MATRIXED, the per-cell invariant, and DIALECT_LEDGER is now empty. A ledger row deliberately does NOT clear MATRIXED: it excuses an undeclared FILE, not a CELL with no matrix-routed suite anywhere. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W6HFzyH98W1YaQXhJUJt6o --- ...-driver-comparand-type-conformance.test.ts | 267 ++++++++++++++---- ...er-icontains-and-retired-operators.test.ts | 45 ++- scripts/check-driver-conformance.mjs | 267 +++++++++++++----- 3 files changed, 455 insertions(+), 124 deletions(-) diff --git a/packages/drivers/driver-sql/src/sql-driver-comparand-type-conformance.test.ts b/packages/drivers/driver-sql/src/sql-driver-comparand-type-conformance.test.ts index 2028c54d30..f7e4f8e107 100644 --- a/packages/drivers/driver-sql/src/sql-driver-comparand-type-conformance.test.ts +++ b/packages/drivers/driver-sql/src/sql-driver-comparand-type-conformance.test.ts @@ -2,7 +2,8 @@ /** * [#7872] `driver-sql` held to `FILTER_COMPARAND_TYPE_CASES` — the - * comparand-type door, both directions, on the compiled-SQL path. + * comparand-type door, both directions, on the compiled-SQL path, across the + * DRIVER axis (ADR-0053 D-A3). * * This driver is one of the two independent implementations the door's set was * MEASURED from (`isBindableComparand` / `isRenderableTextComparand`, whose @@ -13,75 +14,237 @@ * envelope (pinned by `sql-driver-silent-empty-predicate.test.ts` and * siblings). This suite pins the door half, so the shared table drives every * backend identically. + * + * # Why this runs the matrix (#12136, the follow-up #12014 sized) + * + * The case-set's own headline is that the six accepted types **compile + * everywhere** — and until #12136 that sentence was measured on ONE dialect. + * `check-driver-conformance.mjs`'s dialect axis recorded the consequence + * exactly: `FILTER_COMPARAND_TYPE` was the only dialect-scored cell with no + * matrix-routed suite at all, so "everywhere" was a claim the census printed + * and nothing executed. That is #12014's thesis one level in — a suite whose + * NAME says conformance while its COVERAGE says SQLite — and it is why the + * conversion is earned rather than tidy. + * + * ## What is per-dialect here, and what deliberately is not + * + * The case-set has two directions and only one of them has a dialect: + * + * - `matches` / `compiles` cases hand the door-validated condition to THIS + * DRIVER'S execution path, so they are run once per cell. This is the half + * "compile everywhere" is about, and the half that was measured on SQLite + * alone. + * - `door-refusal` cases assert `parseFilterAST` throws BEFORE any driver + * runs. `parseFilterAST` is a pure platform function with no dialect in it + * — running it once per cell would not measure three things, it would + * measure one thing three times and report the repetition as coverage. + * They therefore run once, in the dialect-independent block below, which + * says so in its own name. + * + * # Which cells actually executed, and which did not + * + * Recorded here rather than implied, because a matrix that reports OK while + * finding zero live cells is the failure `declareUnprovisionedCell` exists for + * (#4646): + * + * - **sqlite** — always runs, embedded. + * - **live postgres** — RUN, on PostgreSQL 16.13 (the same version #11456's + * `42883` divergence was measured on). All eight executed cases answered + * the case-set's row ids, so on this dialect "compile everywhere" is now a + * measurement rather than a claim. + * - **live mysql** — NOT run: no MySQL server was provisionable in the + * container this landed from, so it is a declared SKIP, and the MySQL arm + * rests on the compiled-SQL block at the bottom rather than on execution. + * Saying so is the point — the cell is skipped BY NAME, and + * `OS_EXPECT_LIVE_DIALECT_MATRIX=1` turns that skip into a failure for a + * runner that believes it provisioned one. The same disclosure + * `sql-driver-text-case-conformance.test.ts` makes for its own MySQL cell. + * + * # The compiled-SQL layer, and why it is not redundant with the rows + * + * The last block asserts that every accepted comparand type BINDS into a + * statement on each of the three dialects. On the cells that run, rows are the + * stronger witness and the binding is a bonus. On the cell that does NOT run, + * the binding is the only thing this repo can check at all — and it is the + * layer where a type-level refusal would surface, since a comparand this + * driver cannot bind throws out of `applyFilters` before any server is + * reached. knex builds a Postgres or MySQL statement without needing a server + * to send it to, which is what makes the un-provisioned cell checkable here. */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import type { Knex } from 'knex'; import { FILTER_COMPARAND_TYPE_CASES, FILTER_COMPARAND_TYPE_ROWS, parseFilterAST, type FilterCondition, } from '@objectstack/spec/data'; -import { SqlDriver } from '../src/index.js'; +import { SqlDriver, type SqlDriverConfig } from './sql-driver.js'; +import { + DIALECT_CELLS, + declareUnprovisionedCell, + type DialectCell, +} from './live-dialect-matrix.testkit.js'; -const TABLE = 'comparand_conformance'; +/** + * Issue-prefixed object name: the live cells share one database with every + * other suite in this package, so the bare `comparand_conformance` this suite + * carried while it was SQLite-only would be a collision waiting to be read as + * a comparand-type failure. + */ +const TABLE = 'os7872_comparand_type'; + +/** The half of the table that reaches a driver at all — see the head note. */ +const EXECUTED_CASES = FILTER_COMPARAND_TYPE_CASES.filter((c) => c.verdict !== 'door-refusal'); -describe('[#7872] SqlDriver — comparand-type conformance (behind the door)', () => { - let driver: SqlDriver; - let knex: any; +/** The half the door settles upstream of every driver. */ +const DOOR_REFUSAL_CASES = FILTER_COMPARAND_TYPE_CASES.filter((c) => c.verdict === 'door-refusal'); + +// ── The driver axis ───────────────────────────────────────────────────────── + +for (const cell of DIALECT_CELLS) { + if (!cell.available) { + declareUnprovisionedCell(cell, 'FILTER_COMPARAND_TYPE_CASES comparand-type door'); + continue; + } + declareComparandTypeSweep(cell); +} - beforeAll(async () => { - driver = new SqlDriver({ - client: 'better-sqlite3', - connection: { filename: ':memory:' }, - useNullAsDefault: true, +function declareComparandTypeSweep(cell: DialectCell): void { + describe(`[#7872] SqlDriver — comparand-type conformance (${cell.label})`, () => { + let driver: SqlDriver; + let knexInstance: Knex; + + beforeAll(async () => { + driver = new SqlDriver(cell.config()); + // `getKnex()` is public API; the older shape of this file reached the + // `protected` field through `(driver as any).knex`, which erases every + // member of the driver to get one. + knexInstance = driver.getKnex(); + // Live cells reuse one database, so the sweep starts from a dropped table. + await knexInstance.schema.dropTableIfExists(TABLE); + await knexInstance.schema.createTable(TABLE, (t: Knex.TableBuilder) => { + t.string('id').primary(); + t.integer('qty'); + t.string('label'); + t.boolean('active'); + // Nullable (knex's default): the `note: null` case is the declared null + // predicate, and it measures nothing against a NOT NULL column. + t.string('note'); + }); + await knexInstance(TABLE).insert(FILTER_COMPARAND_TYPE_ROWS.map((r) => ({ ...r }))); }); - knex = (driver as any).knex; - await knex.schema.createTable(TABLE, (t: any) => { - t.string('id').primary(); - t.integer('qty'); - t.string('label'); - t.boolean('active'); - t.string('note'); + + afterAll(async () => { + await knexInstance?.schema.dropTableIfExists(TABLE).catch(() => {}); + await driver?.disconnect?.(); }); - await knex(TABLE).insert(FILTER_COMPARAND_TYPE_ROWS.map((r) => ({ ...r }))); - }); - afterAll(async () => { - await knex.destroy(); + const ids = async (where: FilterCondition | undefined): Promise => { + const rows = await driver.find(TABLE, { fields: ['id'], where }); + return rows.map((r: any) => String(r.id)).sort((x, y) => x.localeCompare(y)); + }; + + /** + * The fixture control. Every `matches` case below names row ids, so a cell + * whose insert silently dropped or coerced a row would answer wrong ids for + * a reason that has nothing to do with the comparand door. + */ + it('the fixture really is both rows', async () => { + expect(await ids(undefined)).toEqual(['1', '2']); + }); + + for (const c of EXECUTED_CASES) { + if (c.verdict === 'matches') { + it(c.name, async () => { + expect(await ids(parseFilterAST(c.filter())), c.note).toEqual([...c.expected]); + }); + } else { + it(`${c.name} — executes without refusal`, async () => { + await expect(ids(parseFilterAST(c.filter()))).resolves.toBeDefined(); + }); + } + } }); +} - const ids = async (where: FilterCondition | undefined): Promise => { - const rows = await driver.find(TABLE, { fields: ['id'], where }); - return rows.map((r: any) => String(r.id)).sort((x, y) => x.localeCompare(y)); - }; - - for (const c of FILTER_COMPARAND_TYPE_CASES) { - if (c.verdict === 'door-refusal') { - it(`${c.name} — refused at the door, before any SQL compiles`, () => { - let caught: (Error & { code?: string; status?: number }) | null = null; - try { - parseFilterAST(c.filter()); - } catch (e) { - caught = e as Error & { code?: string; status?: number }; - } - expect(caught, c.note).not.toBeNull(); - expect(caught?.code, c.name).toBe(c.code); - expect(caught?.status, c.name).toBe(400); - for (const fragment of c.mustMention) expect(caught?.message).toContain(fragment); - }); - } else if (c.verdict === 'matches') { - it(c.name, async () => { - expect(await ids(parseFilterAST(c.filter())), c.note).toEqual([...c.expected]); - }); - } else { - it(`${c.name} — executes without refusal`, async () => { - await expect(ids(parseFilterAST(c.filter()))).resolves.toBeDefined(); - }); +// ── The door, which has no dialect ────────────────────────────────────────── + +/** + * `parseFilterAST` runs upstream of every driver and is the same function on + * every cell, so these assertions are declared ONCE rather than once per + * dialect. Repeating a pure function across three cells would add rows to the + * report without adding a measurement — the shape this whole axis exists to + * make visible. + */ +describe('[#7872] the comparand-type door — refused before any dialect is chosen', () => { + for (const c of DOOR_REFUSAL_CASES) { + it(`${c.name} — refused at the door, before any SQL compiles`, () => { + let caught: (Error & { code?: string; status?: number }) | null = null; + try { + parseFilterAST(c.filter()); + } catch (e) { + caught = e as Error & { code?: string; status?: number }; + } + expect(caught, c.note).not.toBeNull(); + // `code` AND `status`: a refusal outside the ADR-0112 envelope reaches + // the client as a 500-shaped body for a 400-class mistake. + expect(caught?.code, c.name).toBe(c.code); + expect(caught?.status, c.name).toBe(400); + for (const fragment of c.mustMention) expect(caught?.message).toContain(fragment); + }); + } +}); + +// ── "compiles everywhere", at the layer a server is not needed for ────────── + +/** + * Every accepted comparand type BINDS into a statement on each dialect. + * + * This is the claim `CASE_SETS` makes about this table in one word, checked on + * all three dialects including the one no server was provisionable for. It is + * a real check rather than a restatement: this driver refuses an unbindable + * comparand from inside `applyFilters` (the `isBindableComparand` gate the + * door's own set was measured from), which throws while the statement is being + * BUILT — before any connection exists. So a type that this driver could not + * bind on `pg` or `mysql2` fails here, with no server involved. + */ +describe('[#7872] every accepted comparand type binds, on every dialect', () => { + /** A driver that exposes the compiled WHERE without reaching into privates. */ + class CompilerProbeDriver extends SqlDriver { + compileWhere(where: FilterCondition): string { + const builder: Knex.QueryBuilder = this.getKnex()(TABLE); + this.applyFilters(builder, where); + return builder.toString(); } } - it('the fixture really is both rows', async () => { - expect(await ids(undefined)).toEqual(['1', '2']); - }); + const probe = (config: SqlDriverConfig) => new CompilerProbeDriver(config); + + const CLIENTS: readonly { id: string; config: SqlDriverConfig }[] = [ + { + id: 'sqlite', + config: { client: 'better-sqlite3', connection: { filename: ':memory:' }, useNullAsDefault: true }, + }, + { id: 'pg', config: { client: 'pg', connection: { host: '127.0.0.1' } } }, + { id: 'mysql', config: { client: 'mysql2', connection: { host: '127.0.0.1' } } }, + ]; + + for (const client of CLIENTS) { + describe(client.id, () => { + for (const c of EXECUTED_CASES) { + it(`${c.name} — binds`, () => { + const sql = probe(client.config).compileWhere(parseFilterAST(c.filter())); + // A statement, with a `where` in it: `applyFilters` throwing is the + // failure this block is looking for, and a builder that silently + // applied NOTHING would render a bare select — which is the quiet + // way a comparand can be dropped rather than refused. + expect(sql, `${client.id} rendered no statement for ${c.name}`).toContain(TABLE); + expect(sql.toLowerCase(), `${client.id} dropped the predicate for ${c.name}`) + .toContain('where'); + }); + } + }); + } }); diff --git a/packages/drivers/driver-sql/src/sql-driver-icontains-and-retired-operators.test.ts b/packages/drivers/driver-sql/src/sql-driver-icontains-and-retired-operators.test.ts index 9f63dc14a5..894f39070a 100644 --- a/packages/drivers/driver-sql/src/sql-driver-icontains-and-retired-operators.test.ts +++ b/packages/drivers/driver-sql/src/sql-driver-icontains-and-retired-operators.test.ts @@ -23,6 +23,40 @@ * `CAST(… AS BINARY)` on MySQL. The per-dialect reasoning and the measurements * behind each cell live on that function. * + * ## [#12136] Deliberately the SQLite cell — measured, not assumed + * + * This suite names `dialectCell('sqlite')`, which declares its narrowness as a + * DECISION rather than leaving it spelled the same way an accident would be + * (#12014). The claim that declaring it loses no dialect coverage was measured + * BY OPERATOR rather than by file, because #6518 makes `FILTER_TEXT_CASES` + * answer differently per dialect (`GLOB` on SQLite, `LIKE` on Postgres, `LIKE` + * over `CAST(… AS BINARY)` on MySQL) and a wrong call here would hide a real + * gap: + * + * - Operators reached by THIS suite, read from comment-masked source: + * `$contains`, `$endsWith`, `$icontains`, `$notContains`, `$options`, + * `$regex`, `$startsWith` — plus `$or`. + * - Operators reached by `FILTER_TEXT_CASES`, read from the EXPORTED case + * filters rather than from the file's prose (its head note also mentions + * `$ilike` and `$not`, which no case actually exercises — the reason this + * was measured off the data): exactly the same seven. + * + * `sql-driver-text-case-conformance.test.ts` runs that whole table once per + * cell of `DIALECT_CELLS`, so all seven are already answered on all three + * dialects. The one extra, `$or`, is a logical combinator belonging to + * `FILTER_LOGIC_CASES`, which `sql-driver-or-filter.test.ts` routes through the + * same matrix. + * + * What is left over — and what makes SQLite the RIGHT cell rather than merely a + * tolerable one — is this file's own residue: the `GLOB` metacharacter class + * (`*`, `?`, `[`), the `lower(name) GLOB lower(…)` construct, and the assertion + * that `$contains` and `$icontains` stop answering identically. `GLOB` is + * emitted on the SQLite dialects and nowhere else, so those blocks are about + * SQLite BY CONSTRUCTION; running them on Postgres or MySQL would assert a + * construct those dialects never compile. The backslash limb is pinned + * per-dialect in `sql-driver-like-escape.test.ts`, which routes through the + * cells itself. + * * ## The reverse verification, direction decided BEFORE it was run * * - **Refusal face** — predicted RED, measured RED. Restoring the deleted @@ -47,6 +81,7 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import type { DriverOptions, FilterCondition } from '@objectstack/spec/data'; import { FILTER_TEXT_CASES, FILTER_TEXT_ROWS } from '@objectstack/spec/data'; import { SqlDriver } from './sql-driver.js'; +import { dialectCell } from './live-dialect-matrix.testkit.js'; /** The error a refused filter produced — never a bare `toThrow()` (see below). */ interface WireBearingError extends Error { @@ -76,11 +111,11 @@ describe('[#5702] SqlDriver — $icontains, and the retired $regex/$options', () let driver: CompilerProbeDriver; beforeAll(async () => { - driver = new CompilerProbeDriver({ - client: 'better-sqlite3', - connection: { filename: ':memory:' }, - useNullAsDefault: true, - }); + // [#12136] The SQLite cell BY NAME, not a hard-coded client literal. The + // value is unchanged — this cell's `config()` is byte-for-byte the literal + // that stood here — but a named cell is a STATED stance, which is the whole + // distinction #12014 found the repo could not spell. + driver = new CompilerProbeDriver(dialectCell('sqlite').config()); await driver.initObjects([{ name: 'txt', fields: { name: { type: 'string' } } }]); for (const row of FILTER_TEXT_ROWS) { await driver.create('txt', { ...row }, BYPASS); diff --git a/scripts/check-driver-conformance.mjs b/scripts/check-driver-conformance.mjs index 6d1491b44c..04bcf341ca 100644 --- a/scripts/check-driver-conformance.mjs +++ b/scripts/check-driver-conformance.mjs @@ -71,6 +71,15 @@ // covered, for a driver that no longer exists, or for a case-set // that no longer exists, is an error. A ledger that can only // accrete rots into a list nobody trusts. +// DIALECTED every conformance suite in a dialect-capable driver SAYS which +// dialects it runs on -- see the dialect-axis block below. +// MATRIXED every dialect-scored cell has at least one MATRIX-ROUTED suite +// (#12136). DIALECTED asks each FILE to state a stance; a tree can +// satisfy it with every suite honestly declaring `sqlite`, and +// then ADR-0053 D-A3's `Postgres at minimum` is enforced nowhere. +// This is the invariant that makes the census ENFORCE D-A3 rather +// than report it, and it is deliberately not buyable from the +// dialect ledger -- see the ledger's own note. // // ## What "covered" means, and what it deliberately does not // @@ -535,11 +544,25 @@ const LEDGER = []; // files that make a cell of THIS census covered — the suites whose coverage // this script's own headline is asserting. // -// Promoting "every dialect-scored cell has at least one matrix-routed suite" -// from a printed measurement to an invariant is the follow-up the numbers below -// size, not something to infer here: today exactly one cell would fail it, and -// converting that suite is a change to what the SUITE asserts, not to what the -// census can see. +// ## MATRIXED: the measurement, promoted (#12136) +// +// "Every dialect-scored cell has at least one matrix-routed suite" was printed +// here as a number (`8 of 9`) before it was enforced. It could not be promoted +// in the same change that started measuring it, because exactly one cell failed +// it -- `FILTER_COMPARAND_TYPE`, whose own CASE_SETS entry says the accepted +// types "compile EVERYWHERE" while it ran on SQLite alone -- and clearing that +// red meant editing a TEST, which is a change to what the suite asserts rather +// than to what the census can see. A gate that ships red is worse than one that +// ships honest, so the promotion waited for the population to reach zero. +// +// #12136 converted that suite to the matrix and measured it on live PG 16.13 +// (all eight executed cases answered the case-set's row ids), which took the +// count to `9 of 9` and made the promotion free. DIALECTED and MATRIXED are two +// different questions and both are needed: DIALECTED is per FILE and asks for a +// STATED stance, MATRIXED is per CELL and asks that the stance somewhere on that +// cell actually be the matrix. A tree where every suite honestly declares +// `dialectCell('sqlite')` passes DIALECTED completely while enforcing D-A3 +// nowhere; MATRIXED is what closes that. // // ## Declared, not detected — so it cannot be respelled around // @@ -573,7 +596,7 @@ const DIALECT_CELL_SYMBOLS = ['dialectCell', 'PG_CELL', 'MYSQL_CELL', 'declareDi /** The export that identifies a package's dialect-matrix testkit, found on disk. */ const DIALECT_TESTKIT_EXPORT = 'DIALECT_CELLS'; -// ── The dialect ledger ────────────────────────────────────────────────────── +// ── The dialect ledger ───────────────────────────────────────── // // One entry per conformance suite that declares no stance. Same discipline as // LEDGER above: every entry is MEASURED against `main`, and clearing one means @@ -581,47 +604,33 @@ const DIALECT_TESTKIT_EXPORT = 'DIALECT_CELLS'; // in both directions, so a row for a suite that now declares — or that no longer // covers a cell — is an error rather than residue. // -// SEEDED, not empty, and the seeding is the first measurement of this axis -// rather than newly written looseness: these two rows are what the population -// WAS on the day the axis was added. Both are deliberately left for a follow-up -// card, because giving either one a stance is a claim about intent this gate's -// author cannot verify and the maintainer can: -// -// comparand-type FILTER_COMPARAND_TYPE_CASES is described in CASE_SETS as -// "the six accepted types compile EVERYWHERE". It is measured -// on one dialect, and it is the only dialect-scored cell with -// no matrix-routed suite at all. Marking it `dialectCell( -// 'sqlite')` would write down the opposite of its own -// case-set's claim. -// icontains FILTER_TEXT_CASES is the case-set whose answer is KNOWN to -// diverge by dialect — #6518 made case sensitivity per-dialect -// (`GLOB` on SQLite, `LIKE` on Postgres, `LIKE` over -// `CAST(… AS BINARY)` on MySQL). A sqlite-only suite over it -// is the shape most likely to be accidental, so "deliberate" -// is exactly the word that must not be guessed. The cell -// itself is matrix-covered by -// `sql-driver-text-case-conformance.test.ts`; this is a second -// suite over the same case-set. - -const DIALECT_LEDGER = [ - { - driver: 'driver-sql', - file: 'packages/drivers/driver-sql/src/sql-driver-comparand-type-conformance.test.ts', - why: 'Hard-codes the SQLite cell. Its case-set claims the accepted types "compile everywhere", ' - + 'and this is the only dialect-scored cell with no matrix-routed suite — so whether it should ' - + 'be marked single-cell or converted to the matrix is a decision, not a marking.', - issue: '#12014', - }, - { - driver: 'driver-sql', - file: 'packages/drivers/driver-sql/src/sql-driver-icontains-and-retired-operators.test.ts', - why: 'Hard-codes the SQLite cell over FILTER_TEXT_CASES, whose answer diverges by dialect ' - + '(#6518: GLOB / LIKE / CAST AS BINARY). The cell is matrix-covered by ' - + 'sql-driver-text-case-conformance.test.ts, so this is a second suite whose narrowness may ' - + 'be deliberate — that is the claim that must be made rather than assumed.', - issue: '#12014', - }, -]; +// EMPTY, and empty is the intended steady state (#12136). It was seeded with two +// rows on the day the axis was added — the population as it stood, not newly +// written looseness — and both are now resolved rather than excused: +// +// comparand-type CONVERTED to the matrix. Its case-set claims the six +// accepted types "compile everywhere", and that sentence had +// been measured on one dialect; it is now measured on the +// cells `DIALECT_CELLS` offers, live PG 16.13 included. +// icontains MARKED `dialectCell('sqlite')`. Measured BY OPERATOR rather +// than by file before marking: the seven text operators it +// reaches are exactly the seven `FILTER_TEXT_CASES` reaches, +// and `sql-driver-text-case-conformance.test.ts` runs that +// whole table once per cell — so the narrowness costs no +// dialect coverage, and its residue (GLOB's escape class, the +// `lower(…) GLOB lower(…)` construct) is about SQLite BY +// CONSTRUCTION. +// +// ⛔ What this ledger can NO LONGER do, now that MATRIXED is enforced. A row +// here excuses an undeclared FILE. It does not — and must not be expected to — +// clear MATRIXED, which is about a CELL: a ledgered suite is not a matrix-routed +// suite, so if the ledgered file is the ONLY suite over its cell, that cell is +// red whatever this list says. The ledger therefore reaches exactly one shape +// now: a second suite over a cell some sibling already routes through the +// matrix. That is a real narrowing of the escape hatch and it is deliberate — +// the promotion exists so D-A3 is enforced rather than excused. + +const DIALECT_LEDGER = []; // ── The ratchet-remedy authority convention (#8435) ───────────────────────── @@ -741,6 +750,38 @@ function dialectedMessage(driver, relFile, markers, kit) { } +/** + * MATRIXED's text, named and pure for the same reason {@link consumedMessage} + * and {@link dialectedMessage} are. + * + * It deliberately offers NO ledger path. The dialect ledger accounts for a FILE + * that cannot state a stance; this error is about a CELL that no suite routes + * through the matrix, and a ledger row cannot make one appear. Saying so in the + * message is the point — an author who went looking for the escape hatch the + * sibling errors offer would otherwise add a row, watch this stay red, and + * conclude the gate is broken. + * + * @param {string} driver + * @param {string} marker the case-set whose cell has no matrix-routed suite + * @param {{file: string, stance: string}[]} suites the suites covering that cell + * @param {{specifier: string, cellIds: string[]}} kit the driver's dialect testkit + * @returns {string} + */ +function matrixedMessage(driver, marker, suites, kit) { + const listed = suites.map((s) => `${s.file.split('/').pop()} (${s.stance})`).join(', '); + return ( + `MATRIXED: ${driver}'s ${marker} cell is covered, but no suite over it runs the dialect ` + + `matrix — ${suites.length} covering suite(s): ${listed}. ADR-0053 D-A3 declares the matrix ` + + `as driver x {SQLite, Postgres at minimum}, so a cell whose every suite names one dialect ` + + `is a cell where "Postgres at minimum" is enforced nowhere. Route one of those suites ` + + `through ${DIALECT_MATRIX_SYMBOLS[0]} from '${kit.specifier}' — it iterates every cell this ` + + `driver speaks (${kit.cellIds.join(', ')}) and reports an unprovisioned one by name rather ` + + `than omitting it. Note this is NOT ledgerable: the dialect ledger excuses a FILE that ` + + 'cannot state a stance, and this is a CELL with no matrix-routed suite anywhere, which no ' + + 'ledger row can supply.' + ); +} + // ── Discovery ─────────────────────────────────────────────────────────────── /** A declared scan root that could not be resolved to a directory. Carries the names. */ @@ -1200,6 +1241,27 @@ function dialectAudit(drivers, coveringByDriver, errors, over = {}) { } } + // MATRIXED — the per-CELL invariant (#12136), promoted from the measurement + // this axis used to only print. Computed HERE rather than in `report()` so + // there is ONE derivation: the numbers the report prints and the numbers this + // invariant enforces cannot disagree, which is the failure a second copy + // would eventually produce silently. + const scoredCells = new Set(); + const matrixCells = new Set(); + for (const s of scored) { + for (const m of s.markers) { + scoredCells.add(`${s.driver}::${m}`); + if (s.stance === 'matrix') matrixCells.add(`${s.driver}::${m}`); + } + } + for (const cellKey of [...scoredCells].filter((c) => !matrixCells.has(c))) { + const [driver, marker] = cellKey.split('::'); + const suites = scored + .filter((s) => s.driver === driver && s.markers.includes(marker)) + .map((s) => ({ file: s.file, stance: s.stance })); + errors.push(matrixedMessage(driver, marker, suites, kits.get(driver))); + } + // RECONCILED, the reverse direction — a dialect-ledger row must still point at // a conformance suite in a dialect-capable driver. for (const entry of ledger) { @@ -1216,7 +1278,7 @@ function dialectAudit(drivers, coveringByDriver, errors, over = {}) { } } - return { scored, singleBackend, kits, notExecutable }; + return { scored, singleBackend, kits, notExecutable, scoredCells, matrixCells }; } function reportDeadRoots(err) { @@ -1267,14 +1329,10 @@ function report() { // Printed BEFORE the error block so a red run still shows the measurement: // the number this axis exists to make visible is most wanted on the run where // something is wrong. - const scoredCells = new Set(); - const matrixCells = new Set(); - for (const s of dialect.scored) { - for (const m of s.markers) { - scoredCells.add(`${s.driver}::${m}`); - if (s.stance === 'matrix') matrixCells.add(`${s.driver}::${m}`); - } - } + // Read from `dialectAudit`, never recomputed: MATRIXED enforces these exact + // two sets, and a second derivation here could print `9 of 9` beside an error + // saying otherwise. + const { scoredCells, matrixCells } = dialect; const singleBackendCells = rows.filter( (r) => r.state === 'covered' && dialect.singleBackend.includes(r.driver), ).length; @@ -1658,6 +1716,10 @@ function selfTest() { const declaredFile = join(dsrc('driver-x'), 'b.test.ts'); writeFileSync(undeclaredFile, "const d = new SqlDriver({ client: 'better-sqlite3' });\n"); writeFileSync(declaredFile, "import { DIALECT_CELLS } from './kit.testkit.js';\nfor (const c of DIALECT_CELLS) {}\n"); + // [#12136] A suite that declares HONESTLY and narrowly — the shape MATRIXED + // exists for. DIALECTED is satisfied by it; D-A3 is not. + const cellFile = join(dsrc('driver-x'), 'd.test.ts'); + writeFileSync(cellFile, "import { dialectCell } from './kit.testkit.js';\nconst c = dialectCell('sqlite');\n"); mkdirSync(dsrc('driver-y'), { recursive: true }); // no testkit: single-backend writeFileSync(join(tmpDialect, 'driver-y', 'package.json'), '{}\n'); writeFileSync(join(dsrc('driver-y'), 'c.test.ts'), 'const x = 1;\n'); @@ -1680,11 +1742,18 @@ function selfTest() { return { errs, out }; }; - // RED: an undeclared conformance suite, with an empty ledger. + // RED: an undeclared conformance suite, with an empty ledger. Two errors + // since #12136, and they are two different findings about one file: it + // states no stance (DIALECTED), and its cell has no matrix-routed suite + // (MATRIXED). Asserted separately so a regression in either is named. const red = drive([[undeclaredFile, ['PAGINATION_CASES']]], []); - expect('an undeclared conformance suite is an error', red.errs.length === 1); - expect('the error names the suite', /a\.test\.ts/.test(red.errs[0] ?? '')); - expect('the error names the dialects it could have declared', /sqlite, pg/.test(red.errs[0] ?? '')); + const redDialected = red.errs.find((e) => e.startsWith('DIALECTED:')); + const redMatrixed = red.errs.find((e) => e.startsWith('MATRIXED:')); + expect('an undeclared conformance suite is an error', red.errs.length === 2); + expect('DIALECTED names the suite', /a\.test\.ts/.test(redDialected ?? '')); + expect('DIALECTED names the dialects it could have declared', /sqlite, pg/.test(redDialected ?? '')); + expect('#12136 — MATRIXED also fires, naming the CELL rather than the file', + /PAGINATION_CASES/.test(redMatrixed ?? '')); // GREEN: the same suite, declared. Same tree, same call — so the red above // was caused by the missing stance and nothing else. @@ -1692,11 +1761,46 @@ function selfTest() { expect('declaring a stance clears it', green.errs.length === 0); expect('and the stance is recorded as matrix', green.out.scored[0]?.stance === 'matrix'); - // GREEN by ledger — and REPORTED as ledgered rather than as covered-and-fine. + // GREEN by ledger, for DIALECTED — and REPORTED as ledgered rather than as + // covered-and-fine. Since #12136 the ledger clears DIALECTED and NOTHING + // ELSE: this file is the only suite over its cell, so MATRIXED still fires. + // That narrowing is the promotion's whole point (a ledger row must not be a + // way to buy D-A3 green), so it is pinned here rather than left to be + // discovered by someone adding a row and finding it did not work. const ledgered = drive([[undeclaredFile, ['PAGINATION_CASES']]], [{ driver: 'driver-x', file: 'driver-x/src/a.test.ts', why: 'measured', issue: '#0' }]); - expect('a ledger entry accounts for an undeclared suite', ledgered.errs.length === 0); + expect('a ledger entry accounts for an undeclared suite (no DIALECTED error)', + !ledgered.errs.some((e) => e.startsWith('DIALECTED:'))); expect('and it is REPORTED as ledgered, never as declared', ledgered.out.scored[0]?.stance === 'ledger'); + expect('#12136 — but a ledger row does NOT clear MATRIXED: a ledgered suite is not a ' + + 'matrix-routed one, so its cell is still uncovered on the dialect axis', + ledgered.errs.length === 1 && ledgered.errs[0].startsWith('MATRIXED:')); + expect('#12136 — and MATRIXED says so, so nobody adds a second row expecting it to work', + /NOT ledgerable/.test(ledgered.errs[0] ?? '')); + + // -- MATRIXED, both directions, over the same synthetic tree. -- + // + // The direction that matters: a suite can satisfy DIALECTED completely and + // still leave D-A3 enforced nowhere. This is the tree #12136 promotes the + // invariant against. + const honestlyNarrow = drive([[cellFile, ['PAGINATION_CASES']]], []); + expect('#12136 — a named-cell suite states a stance, so DIALECTED is satisfied', + !honestlyNarrow.errs.some((e) => e.startsWith('DIALECTED:'))); + expect('#12136 — and its stance is recorded as cell', honestlyNarrow.out.scored[0]?.stance === 'cell'); + expect('#12136 — but MATRIXED is RED: the cell has no matrix-routed suite', + honestlyNarrow.errs.length === 1 && honestlyNarrow.errs[0].startsWith('MATRIXED:')); + expect('#12136 — and the message names the covering suite and its stance', + /d\.test\.ts \(cell\)/.test(honestlyNarrow.errs[0] ?? '')); + + // GREEN: the same narrow suite, beside a matrix-routed sibling over the SAME + // cell — the arrangement FILTER_TEXT is in on the real tree. MATRIXED is per + // CELL, so the sibling satisfies it and the narrow suite is not an error. + const narrowWithSibling = drive( + [[cellFile, ['PAGINATION_CASES']], [declaredFile, ['PAGINATION_CASES']]], []); + expect('#12136 — a matrix-routed sibling over the same cell clears MATRIXED', + narrowWithSibling.errs.length === 0); + expect('#12136 — and the cell is counted once, as matrix-covered', + narrowWithSibling.out.matrixCells.size === 1 && narrowWithSibling.out.scoredCells.size === 1); // RECONCILED, all three directions. const stale = drive([[declaredFile, ['PAGINATION_CASES']]], @@ -1776,10 +1880,34 @@ function selfTest() { expect('driver-sql is discovered as dialect-capable from disk', liveKit !== null); expect('and D-A3\'s two minimum dialects are both cells of it ("SQLite, Postgres at minimum")', ['sqlite', 'pg'].every((id) => liveKit?.cellIds.includes(id))); - expect('every row of the dialect ledger points at a file that exists', - DIALECT_LEDGER.every((e) => { - try { return statSync(join(ROOT, e.file)).isFile(); } catch { return false; } - })); + // `[].every()` is TRUE, so with the ledger at its intended empty steady state + // this assertion measures nothing — the shape #12136 had to decide about + // rather than leave as a green that had quietly stopped checking. Both facts + // are asserted, and which one is live is stated in its own label, so a future + // row is checked and today's emptiness is not read as a passing file check. + if (DIALECT_LEDGER.length === 0) { + expect('#12136 — the dialect ledger is EMPTY, which is the steady state the promoted ' + + 'MATRIXED invariant assumes (a row cannot clear it, so a non-empty ledger means a cell ' + + 'is being excused that this gate no longer excuses)', true); + } else { + expect('every row of the dialect ledger points at a file that exists', + DIALECT_LEDGER.every((e) => { + try { return statSync(join(ROOT, e.file)).isFile(); } catch { return false; } + })); + } + // The real tree must actually satisfy the invariant this script now enforces — + // asserted here as well as in `report()`, so `--self-test` cannot pass on a + // tree whose census is red. + { + const errs = []; + const live = audit(); + expect('#12136 — MATRIXED holds on the real tree: every dialect-scored cell has a ' + + 'matrix-routed suite', live.dialect.scoredCells.size === live.dialect.matrixCells.size); + expect('#12136 — and that population is not empty (an axis that scored nothing would ' + + 'satisfy the invariant vacuously)', live.dialect.scoredCells.size > 0); + expect('#12136 — the real tree raises no MATRIXED error', + !live.errors.some((e) => e.startsWith('MATRIXED:')) && errs.length === 0); + } if (failures.length) { for (const f of failures) console.error(` x self-test: ${f}`); @@ -1796,7 +1924,12 @@ function selfTest() { + 'regexes survive stripping, a matrix / named-cell / undeclared reading is pinned in all ' + 'three directions, an undeclared conformance suite is RED and declaring one is GREEN over ' + 'the same synthetic tree, the dialect ledger reconciles in all three directions, and its ' - + 'offer is marked maintainer-only too.', + + 'offer is marked maintainer-only too. And it holds MATRIXED (#12136), the per-CELL ' + + 'invariant: a suite that declares HONESTLY and narrowly satisfies DIALECTED while leaving ' + + 'its cell without a matrix-routed suite, which is RED; a matrix-routed sibling over the ' + + 'same cell is GREEN; a dialect-ledger row does NOT clear it, and the message says so; and ' + + 'the invariant is asserted against the real tree over a non-empty population, so it cannot ' + + 'pass vacuously.', ); } From eee427ed4c8e0c99df674c1941dc9178d3c8b7a8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 17:03:02 +0000 Subject: [PATCH 2/2] test(driver-sql): assert the parsed condition before compiling it in the bind probe `parseFilterAST` is typed `FilterCondition | undefined`, which `tsc --noEmit` caught and the vitest run could not. Asserted rather than `!`-ed: a case that produced no condition would compile a bare select, and the binding assertions would then be measuring an empty statement rather than a bound comparand. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W6HFzyH98W1YaQXhJUJt6o --- .../src/sql-driver-comparand-type-conformance.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/drivers/driver-sql/src/sql-driver-comparand-type-conformance.test.ts b/packages/drivers/driver-sql/src/sql-driver-comparand-type-conformance.test.ts index f7e4f8e107..8797a8ee5e 100644 --- a/packages/drivers/driver-sql/src/sql-driver-comparand-type-conformance.test.ts +++ b/packages/drivers/driver-sql/src/sql-driver-comparand-type-conformance.test.ts @@ -235,7 +235,13 @@ describe('[#7872] every accepted comparand type binds, on every dialect', () => describe(client.id, () => { for (const c of EXECUTED_CASES) { it(`${c.name} — binds`, () => { - const sql = probe(client.config).compileWhere(parseFilterAST(c.filter())); + // `parseFilterAST` is typed `FilterCondition | undefined`. Asserted + // rather than `!`-ed: a case that produced NO condition would compile + // a bare select, and every assertion below would then be measuring an + // empty statement instead of a bound comparand. + const condition = parseFilterAST(c.filter()); + expect(condition, `${c.name} produced no condition to compile`).toBeDefined(); + const sql = probe(client.config).compileWhere(condition as FilterCondition); // A statement, with a `where` in it: `applyFilters` throwing is the // failure this block is looking for, and a builder that silently // applied NOTHING would render a bare select — which is the quiet