From a2bd07c7aa6d3d5d7722dd9e6249ef85c6990b6e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 22:20:07 +0000 Subject: [PATCH] test(driver-turso): drive FILTER_LOGIC / PAGINATION shared case-sets on BOTH transports, clearing the three DEBT cells (#5590) `TursoDriver` is dual-transport. Local/replica inherits SqlDriver's filter compiler and its paged-read tie-breaker; remote does not go through knex at all -- `src/remote-transport.ts` carries its own `buildWhereSQL` and its own ORDER BY / LIMIT / OFFSET assembly. That is the independent Nth backend objectstack#3774 and objectstack#4363 wrote the shared case-sets for, so each cell takes two suites, one per transport, in the shape the temporal pair in this package already established: turso-filter-logic-conformance.test.ts local, :memory: turso-remote-filter-logic-conformance.test.ts remote, sqlite stub turso-pagination-conformance.test.ts local, :memory: turso-remote-pagination-conformance.test.ts remote, sqlite stub All four are hermetic -- the remote half runs over `libsql-sqlite-stub.testkit.ts`, so no network and no credentials -- and all four are green, which is what lets the three DEBT entries leave the ledger in this same commit (25 covered cells, 0 DEBT). The remote PAGINATION half passes WITHOUT the mechanism the contract names: `buildSelectSQL` maps the caller's `orderBy` verbatim and appends no unique column, so the cases hold on a twelve-row better-sqlite3 table rather than by a promise the transport makes. Filed as objectstack#5653 and stated plainly in both the suite's module doc and the gate's ledger note; two `records the measured mechanism` tests pin the current no-tie-breaker behaviour so it cannot go quiet under a green cell. Per this issue's boundary the transport itself is untouched here. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WyvqvKMG6asi9aXjKE6xtx --- .../turso-filter-logic-conformance.test.ts | 91 +++++++++ .../src/turso-pagination-conformance.test.ts | 128 +++++++++++++ ...so-remote-filter-logic-conformance.test.ts | 119 ++++++++++++ ...urso-remote-pagination-conformance.test.ts | 178 ++++++++++++++++++ scripts/check-driver-conformance.mjs | 96 +++++----- 5 files changed, 562 insertions(+), 50 deletions(-) create mode 100644 packages/drivers/driver-turso/src/turso-filter-logic-conformance.test.ts create mode 100644 packages/drivers/driver-turso/src/turso-pagination-conformance.test.ts create mode 100644 packages/drivers/driver-turso/src/turso-remote-filter-logic-conformance.test.ts create mode 100644 packages/drivers/driver-turso/src/turso-remote-pagination-conformance.test.ts diff --git a/packages/drivers/driver-turso/src/turso-filter-logic-conformance.test.ts b/packages/drivers/driver-turso/src/turso-filter-logic-conformance.test.ts new file mode 100644 index 0000000000..2585c41277 --- /dev/null +++ b/packages/drivers/driver-turso/src/turso-filter-logic-conformance.test.ts @@ -0,0 +1,91 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Filter logical-combinator conformance for TursoDriver in LOCAL mode (#3774, + * #5590) — the shared `@objectstack/spec/data` cases, run through this + * driver's own pipeline. + * + * `TursoDriver extends SqlDriver`, so in local (and replica — same local + * engine) mode the `$and` / `$or` / `$not` compilation is inherited and + * nothing here re-implements it. Two things this file pins are nonetheless + * this package's own, and would fail in no other suite in the repo: + * + * 1. **The transport router.** Every read method on this driver is an + * `override` that branches on `this.isRemote` before it reaches + * `super.find`. A branch that sent a local read down the remote path — or + * a `toRemoteQuery` that ran on a query it was never meant to touch — + * changes which compiler answers a filter, and only a row-result assertion + * can see that. + * 2. **The temporal seam the constructor installs.** `filterColumnSql` + * rewrites the SQL on the LEFT of a comparison for temporal columns + * (ADR-0053 D-A1, #937). Every column in {@link FILTER_LOGIC_ROWS} is a + * plain string, so the seam must leave all of them alone; a rewrite that + * over-reached would corrupt exactly the boring predicates this table is + * built from. + * + * "It inherits the compiler, therefore it is fine" is the assumption the + * shared case-sets exist to disprove — `driver-sqlite-wasm` recorded this same + * cell as DEBT for that reason and cleared it with a suite, not with the + * sentence. The REMOTE half of this driver inherits nothing at all and gets + * its own file (`turso-remote-filter-logic-conformance.test.ts`), the same + * two-transport shape the temporal suites next door already use. + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { FILTER_LOGIC_CASES, FILTER_LOGIC_ROWS } from '@objectstack/spec/data'; +import { TursoDriver } from './turso-driver.js'; + +const CONFORMANCE_OBJECT = { + name: 'conformance', + fields: { + a: { type: 'string' }, + b: { type: 'string' }, + c: { type: 'string' }, + owner: { type: 'string' }, + status: { type: 'string' }, + parent_object: { type: 'string' }, + parent_id: { type: 'string' }, + }, +}; + +const ids = (rows: Array>): string[] => + rows.map((r) => String(r.id)).sort((x, y) => x.localeCompare(y)); + +describe('TursoDriver — filter logic conformance (local mode)', () => { + let driver: TursoDriver; + + beforeAll(async () => { + driver = new TursoDriver({ url: ':memory:' }); + // The mode this suite is about — the SqlDriver-inherited engine. Replica + // shares it; remote does not (see module doc). + expect(driver.transportMode).toBe('local'); + await driver.initObjects([CONFORMANCE_OBJECT]); + for (const row of FILTER_LOGIC_ROWS) { + await driver.create('conformance', { ...row }, { bypassTenantAudit: true }); + } + }); + + afterAll(async () => { + await driver.disconnect(); + }); + + /** + * The fixture as a whole, first: a case that returns nothing because the + * seed failed must not read as a case that correctly excluded everything. + */ + it('the fixture really is all four rows', async () => { + const rows = await driver.find('conformance', { object: 'conformance' }); + expect(ids(rows)).toEqual(['1', '2', '3', '4']); + }); + + for (const c of FILTER_LOGIC_CASES) { + it(c.name, async () => { + const rows = await driver.find( + 'conformance', + { object: 'conformance', where: c.filter }, + { bypassTenantAudit: true }, + ); + expect(ids(rows), c.note).toEqual([...c.expected]); + }); + } +}); diff --git a/packages/drivers/driver-turso/src/turso-pagination-conformance.test.ts b/packages/drivers/driver-turso/src/turso-pagination-conformance.test.ts new file mode 100644 index 0000000000..c9e749c206 --- /dev/null +++ b/packages/drivers/driver-turso/src/turso-pagination-conformance.test.ts @@ -0,0 +1,128 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Deterministic paged reads for TursoDriver in LOCAL mode (objectui#3106, + * #4363, #5590) — the contract on `IDataDriver.find`, run against the shared + * `@objectstack/spec/data` cases like every other driver. + * + * `TursoDriver extends SqlDriver`, so the tie-breaking ORDER BY is *built* by + * inherited code and nothing here re-implements it. What this pins is that it + * still reaches the engine on this driver: every read method is an `override` + * that branches on `this.isRemote` before delegating to `super.find`, so the + * inherited clause travels through one more layer here than it does in the + * base package. A router that mangled or bypassed the query — or a future + * override that rebuilt the paged read itself — would produce exactly the + * failure the contract rules out (full pages, real rows, one served twice and + * one never served), and it would fail in no other suite in the repo. + * + * The REMOTE transport keeps its own file + * (`turso-remote-pagination-conformance.test.ts`): it does not go through knex + * at all and assembles its own ORDER BY / LIMIT / OFFSET, which is a second + * implementation of this contract rather than a second engine under the same + * one. + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { + PAGINATION_ALL_IDS, + PAGINATION_CASES, + PAGINATION_ROWS, + PAGINATION_UNORDERED_CASES, +} from '@objectstack/spec/data'; +import { TursoDriver } from './turso-driver.js'; + +const TICKET_OBJECT = { + name: 'ticket', + fields: { + status: { type: 'string' }, + rank: { type: 'integer' }, + name: { type: 'string' }, + }, +}; + +describe('TursoDriver — paged reads are a partition of the result set (local mode)', () => { + let driver: TursoDriver; + + beforeAll(async () => { + driver = new TursoDriver({ url: ':memory:' }); + expect(driver.transportMode).toBe('local'); + await driver.initObjects([TICKET_OBJECT]); + for (const row of PAGINATION_ROWS) { + await driver.create('ticket', { ...row }, { bypassTenantAudit: true }); + } + }); + + afterAll(async () => { + await driver.disconnect(); + }); + + /** Walk the whole table page by page, collecting the ids in visit order. */ + const walk = async ( + pageSize: number, + orderBy?: ReadonlyArray<{ field: string; order: 'asc' | 'desc' }>, + ): Promise => { + const seen: string[] = []; + for (let offset = 0; offset < PAGINATION_ROWS.length; offset += pageSize) { + const page: Array> = await driver.find( + 'ticket', + { ...(orderBy ? { orderBy: [...orderBy] } : {}), limit: pageSize, offset }, + { bypassTenantAudit: true }, + ); + seen.push(...page.map((r) => String(r.id))); + } + return seen; + }; + + it('the fixture really is all twelve rows', async () => { + const rows: Array> = await driver.find( + 'ticket', + { object: 'ticket' }, + { bypassTenantAudit: true }, + ); + expect(rows.map((r) => String(r.id)).sort()).toEqual([...PAGINATION_ALL_IDS].sort()); + }); + + for (const testCase of PAGINATION_CASES) { + it(`visits every row exactly once — ${testCase.name}`, async () => { + const seen = await walk(testCase.pageSize, testCase.orderBy); + expect(seen).toHaveLength(PAGINATION_ALL_IDS.length); + expect(new Set(seen).size).toBe(PAGINATION_ALL_IDS.length); + expect([...seen].sort()).toEqual([...PAGINATION_ALL_IDS].sort()); + }); + + it(`page boundaries are invisible — ${testCase.name}`, async () => { + const paged = await walk(testCase.pageSize, testCase.orderBy); + const whole: Array> = await driver.find( + 'ticket', + { orderBy: [...testCase.orderBy] }, + { bypassTenantAudit: true }, + ); + expect(paged).toEqual(whole.map((r) => String(r.id))); + }); + } + + for (const testCase of PAGINATION_UNORDERED_CASES) { + it(`visits every row exactly once with NO orderBy at all — ${testCase.name}`, async () => { + const seen = await walk(testCase.pageSize); + expect(seen).toHaveLength(PAGINATION_ALL_IDS.length); + expect(new Set(seen).size).toBe(PAGINATION_ALL_IDS.length); + expect([...seen].sort()).toEqual([...PAGINATION_ALL_IDS].sort()); + }); + + it(`walks an unsorted read in id order — ${testCase.name}`, async () => { + // The fixture's ids are shuffled relative to insertion order, so this + // distinguishes "the inherited tie-breaking ORDER BY reached the engine" + // from "SQLite happened to hand back rowid order". + expect(await walk(testCase.pageSize)).toEqual([...PAGINATION_ALL_IDS].sort()); + }); + } + + it('leaves an UNPAGED unordered read alone — no sort is imposed on a caller who asked for none', async () => { + const rows: Array> = await driver.find( + 'ticket', + {}, + { bypassTenantAudit: true }, + ); + expect(rows.map((r) => String(r.id))).toEqual(PAGINATION_ROWS.map((r) => r.id)); + }); +}); diff --git a/packages/drivers/driver-turso/src/turso-remote-filter-logic-conformance.test.ts b/packages/drivers/driver-turso/src/turso-remote-filter-logic-conformance.test.ts new file mode 100644 index 0000000000..3bf538c87f --- /dev/null +++ b/packages/drivers/driver-turso/src/turso-remote-filter-logic-conformance.test.ts @@ -0,0 +1,119 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Filter logical-combinator conformance for TursoDriver's REMOTE transport + * (#3774, #5590) — the shared `@objectstack/spec/data` cases, on rows. + * + * The local twin of this suite passes largely by INHERITANCE: `TursoDriver + * extends SqlDriver`, so `applyFilterCondition` compiles the combinators. + * Remote mode inherits none of it. `RemoteTransport.buildWhereSQL` + * (`src/remote-transport.ts`) is an independent filter compiler — its own + * `$and` / `$or` / `$not` nesting, its own operator vocabulary, its own + * comparand refusal, its own identity elements for the empty combinators. It + * is the "independent Nth backend" #3774 wrote this table for, and the seam + * has demonstrably diverged before: six semantic fixes landed in this one + * function while the package lived in `objectstack-ai/cloud` (#1071, #1074, + * #1078, #1080, #1116, #1117), every one of them a case where remote answered + * a filter differently from local. + * + * That history is also why the boolean-identity rows matter here more than + * anywhere else. `$and: []` is TRUE, `$or: []` is FALSE, a `{}` disjunct + * absorbs its `$or`, and `$not: {}` is FALSE (the #5322 ruling) — this + * transport reaches each of those through a *hand-written* branch rather than + * through knex, and "compiled to no clause" is the same string for TRUE and + * for "something was silently dropped". A dropped predicate leaves valid SQL, + * just wider, so it is invisible to a SQL-string assertion; only the row set + * tells them apart, which is what this file compares. + * + * ## Why a SQLite-backed client stub + * + * Same reason as the remote temporal suite next door: libsql IS SQLite, so + * `makeLibsqlSqliteStub` gives the transport real value and ordering semantics + * with no network and no credentials. The network itself stays the concern of + * the suites that mock `execute` and assert on the SQL string. + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { FILTER_LOGIC_CASES, FILTER_LOGIC_ROWS } from '@objectstack/spec/data'; +import { TursoDriver } from './turso-driver.js'; +import { makeLibsqlSqliteStub, type LibsqlSqliteStub } from './libsql-sqlite-stub.testkit.js'; + +const CONFORMANCE_OBJECT = { + name: 'conformance', + fields: { + a: { type: 'string' }, + b: { type: 'string' }, + c: { type: 'string' }, + owner: { type: 'string' }, + status: { type: 'string' }, + parent_object: { type: 'string' }, + parent_id: { type: 'string' }, + }, +}; + +const ids = (rows: Array>): string[] => + rows.map((r) => String(r.id)).sort((x, y) => x.localeCompare(y)); + +describe('TursoDriver remote — filter logic conformance', () => { + let driver: TursoDriver; + let stub: LibsqlSqliteStub; + + beforeAll(async () => { + stub = makeLibsqlSqliteStub(); + driver = new TursoDriver({ url: 'libsql://conformance.turso.io', client: stub as never }); + await driver.connect(); + // The mode this suite is about — the one that inherits nothing. + expect(driver.transportMode).toBe('remote'); + // `syncSchema` is what creates the table and registers the field metadata + // in remote mode (`registerRemoteFieldMetadata`); there is no `initObjects` + // DDL path here. + await driver.syncSchema(CONFORMANCE_OBJECT.name, CONFORMANCE_OBJECT); + for (const row of FILTER_LOGIC_ROWS) { + await driver.create('conformance', { ...row }); + } + }); + + afterAll(async () => { + await driver.disconnect(); + stub.close(); + }); + + /** + * The fixture as a whole, first — and read below the transport, so a case + * that returns nothing because the seed never landed cannot read as a case + * that correctly excluded everything. + */ + it('the fixture really is all four rows, as stored', () => { + const rows = stub.raw.prepare('select id, a, b, c from conformance order by id').all() as Array<{ + id: string; + a: string; + b: string; + c: string; + }>; + expect(rows.map((r) => r.id)).toEqual(['1', '2', '3', '4']); + for (const row of rows) { + const seeded = FILTER_LOGIC_ROWS.find((r) => r.id === row.id)!; + expect([row.a, row.b, row.c], row.id).toEqual([seeded.a, seeded.b, seeded.c]); + } + }); + + for (const c of FILTER_LOGIC_CASES) { + it(c.name, async () => { + const rows = await driver.find('conformance', { where: c.filter }); + expect(ids(rows), c.note).toEqual([...c.expected]); + }); + } + + /** + * `count()` compiles its WHERE through the same `buildWhereSQL` but a + * different statement builder, so a combinator that is right for `find` can + * still be wrong for `count` — which is how a list view shows a page of rows + * under a total that disagrees with it. One assertion over the whole table + * rather than a second copy of it. + */ + it('count() answers the same row set find() does, case for case', async () => { + for (const c of FILTER_LOGIC_CASES) { + expect(await driver.count('conformance', { where: c.filter }), c.name).toBe(c.expected.length); + } + }); +}); diff --git a/packages/drivers/driver-turso/src/turso-remote-pagination-conformance.test.ts b/packages/drivers/driver-turso/src/turso-remote-pagination-conformance.test.ts new file mode 100644 index 0000000000..dff2c6c9c9 --- /dev/null +++ b/packages/drivers/driver-turso/src/turso-remote-pagination-conformance.test.ts @@ -0,0 +1,178 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Deterministic paged reads for TursoDriver's REMOTE transport (objectui#3106, + * #4363, #5590) — the shared `@objectstack/spec/data` cases, on rows. + * + * The local twin of this suite passes by INHERITANCE: `TursoDriver extends + * SqlDriver`, so `orderKeysFor()` appends the `id` tie-breaker to every paged + * read. Remote mode inherits nothing — `RemoteTransport.buildSelectSQL` + * assembles its own ORDER BY / LIMIT / OFFSET — which makes it a second + * implementation of this contract inside ONE driver, selected by URL alone. + * That is exactly the shape #4363 wrote these cases for. + * + * ## What this suite measured, stated plainly + * + * Both case-sets pass here, and they do NOT pass for the reason the local + * twin's do. `buildSelectSQL` maps the caller's `orderBy` entries verbatim and + * appends no unique column, so: + * + * - a sorted paged read goes out as `ORDER BY status LIMIT ? OFFSET ?`, and + * the ties come back in storage order rather than id order; + * - an unsorted paged read goes out with no ORDER BY at all. + * + * The property holds on this fixture because the stub is `better-sqlite3` over + * a twelve-row in-memory table: one plan, one arrangement, every time. On a + * real endpoint the arrangement of equal keys across two statements is not + * promised — the case-set's own module doc says so, and names the unsorted + * read as the same defect at full strength rather than as an exemption. The + * `driver-memory` carve-out ("storage order steady between reads") is about a + * JS array, not a SQL plan. + * + * So the honest reading of a green run here is: **the transport currently + * satisfies the cases without implementing the mechanism the contract asks + * for.** That gap is filed as #5653, and the two `records the measured + * mechanism` tests below pin it — they assert the tie arrangement IS storage + * order, so the day #5653 lands they go red and get updated with it, instead + * of the divergence sitting here undocumented under a green suite. Fixing the + * transport is deliberately not this file's job (#5590's boundary: write the + * suite, do not grade your own paper). + * + * ## Why a SQLite-backed client stub + * + * Same instrument, same reason as the remote temporal and filter-logic suites: + * these are row-order assertions, and a lost ORDER BY leaves the SQL perfectly + * valid — so a SQL-string assertion sails past it. libsql IS SQLite, so the + * stub gives the transport real ordering semantics with no network and no + * credentials. + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { + PAGINATION_ALL_IDS, + PAGINATION_CASES, + PAGINATION_ROWS, + PAGINATION_UNORDERED_CASES, +} from '@objectstack/spec/data'; +import { TursoDriver } from './turso-driver.js'; +import { makeLibsqlSqliteStub, type LibsqlSqliteStub } from './libsql-sqlite-stub.testkit.js'; + +const TICKET_OBJECT = { + name: 'ticket', + fields: { + status: { type: 'string' }, + rank: { type: 'integer' }, + name: { type: 'string' }, + }, +}; + +describe('TursoDriver remote — paged reads are a partition of the result set', () => { + let driver: TursoDriver; + let stub: LibsqlSqliteStub; + + beforeAll(async () => { + stub = makeLibsqlSqliteStub(); + driver = new TursoDriver({ url: 'libsql://conformance.turso.io', client: stub as never }); + await driver.connect(); + // The mode this suite is about — the one that assembles its own paging. + expect(driver.transportMode).toBe('remote'); + await driver.syncSchema(TICKET_OBJECT.name, TICKET_OBJECT); + for (const row of PAGINATION_ROWS) { + await driver.create('ticket', { ...row }); + } + }); + + afterAll(async () => { + await driver.disconnect(); + stub.close(); + }); + + /** Walk the whole table page by page, collecting the ids in visit order. */ + const walk = async ( + pageSize: number, + orderBy?: ReadonlyArray<{ field: string; order: 'asc' | 'desc' }>, + ): Promise => { + const seen: string[] = []; + for (let offset = 0; offset < PAGINATION_ROWS.length; offset += pageSize) { + const page: Array> = await driver.find('ticket', { + ...(orderBy ? { orderBy: [...orderBy] } : {}), + limit: pageSize, + offset, + }); + seen.push(...page.map((r) => String(r.id))); + } + return seen; + }; + + /** + * Read below the transport first: a walk that visits nothing because the + * seed never landed would satisfy every set comparison in this file for the + * wrong reason. + */ + it('the fixture really is all twelve rows, as stored', () => { + const rows = stub.raw.prepare('select id from ticket').all() as Array<{ id: string }>; + expect(rows.map((r) => r.id).sort()).toEqual([...PAGINATION_ALL_IDS].sort()); + }); + + for (const testCase of PAGINATION_CASES) { + it(`visits every row exactly once — ${testCase.name}`, async () => { + const seen = await walk(testCase.pageSize, testCase.orderBy); + expect(seen).toHaveLength(PAGINATION_ALL_IDS.length); + expect(new Set(seen).size).toBe(PAGINATION_ALL_IDS.length); + expect([...seen].sort()).toEqual([...PAGINATION_ALL_IDS].sort()); + }); + + it(`page boundaries are invisible — ${testCase.name}`, async () => { + const paged = await walk(testCase.pageSize, testCase.orderBy); + const whole: Array> = await driver.find('ticket', { + orderBy: [...testCase.orderBy], + }); + expect(paged).toEqual(whole.map((r) => String(r.id))); + }); + } + + for (const testCase of PAGINATION_UNORDERED_CASES) { + it(`visits every row exactly once with NO orderBy at all — ${testCase.name}`, async () => { + const seen = await walk(testCase.pageSize); + expect(seen).toHaveLength(PAGINATION_ALL_IDS.length); + expect(new Set(seen).size).toBe(PAGINATION_ALL_IDS.length); + expect([...seen].sort()).toEqual([...PAGINATION_ALL_IDS].sort()); + }); + + it(`page boundaries are invisible with NO orderBy — ${testCase.name}`, async () => { + const paged = await walk(testCase.pageSize); + const whole: Array> = await driver.find('ticket', {}); + expect(paged).toEqual(whole.map((r) => String(r.id))); + }); + } + + it('leaves an UNPAGED unordered read alone — no sort is imposed on a caller who asked for none', async () => { + const rows: Array> = await driver.find('ticket', {}); + expect(rows.map((r) => String(r.id))).toEqual(PAGINATION_ROWS.map((r) => r.id)); + }); + + /** + * The two pins. See the module doc: the cases above pass, but not by the + * mechanism the contract names, and a green suite that leaves that unsaid is + * how "covered" quietly stops meaning anything. Both assert the CURRENT + * behaviour and are expected to go red — and be rewritten — the day #5653 + * gives this transport the tie-breaker local mode already has. + */ + it('records the measured mechanism: a sorted paged read appends NO tie-breaker (#5653)', async () => { + const seen = await walk(5, [{ field: 'status', order: 'asc' }]); + // What the caller's key alone produces: the `status` groups in order, and + // INSIDE each group the rows in storage (insertion) order. With the `id` + // tie-breaker local mode appends, the `done` group would instead read + // r02,r03,r09,r10 — id order. + const groupedByStatusThenInsertion = PAGINATION_ROWS.map((row, index) => ({ row, index })) + .sort((x, y) => x.row.status.localeCompare(y.row.status) || x.index - y.index) + .map(({ row }) => row.id); + expect(seen).toEqual(groupedByStatusThenInsertion); + }); + + it('records the measured mechanism: an unsorted paged read is served in storage order, not id order (#5653)', async () => { + const seen = await walk(5); + expect(seen).toEqual(PAGINATION_ROWS.map((r) => r.id)); + expect(seen).not.toEqual([...PAGINATION_ALL_IDS].sort()); + }); +}); diff --git a/scripts/check-driver-conformance.mjs b/scripts/check-driver-conformance.mjs index 7f1350d538..fbb1867367 100644 --- a/scripts/check-driver-conformance.mjs +++ b/scripts/check-driver-conformance.mjs @@ -137,8 +137,12 @@ const CASE_SETS = [ // covered, is not yet) or EXEMPT (cannot meaningfully apply). Both are measured // claims; neither is a default. // -// EMPTY, as of #4405 — every cell of the matrix is covered by a suite. The two -// FILTER_LOGIC_CASES rows this ledger opened with are both cleared: +// EMPTY, as of #5590 — every cell of the matrix is covered by a suite. Five +// entries have passed through here across two generations, and every one of +// them was cleared the same way: by writing the suite, never by the argument +// that predicted the suite was unnecessary. +// +// The two FILTER_LOGIC_CASES rows this ledger opened with (#4405): // // driver-mongodb `translateFilter` was the independent fifth backend // #3774 never enrolled when it named "the four". It now @@ -154,6 +158,45 @@ const CASE_SETS = [ // the assumption those suites exist to disprove; the suite // is what disproves it, not the entry. // +// The three driver-turso rows Phase A of #4645 measured on arrival, cleared by +// #5590. All three were DEBT for one reason — the driver is DUAL-TRANSPORT. +// Local/replica inherits SqlDriver's filter compiler and its paging; remote +// does not go through knex at all, and `src/remote-transport.ts` carries its +// own `buildWhereSQL` (combinator nesting, operator vocabulary, comparand +// refusal) and its own ORDER BY / LIMIT / OFFSET assembly. That is an +// independent Nth backend, which is what #3774 and #4363 wrote the case-sets +// for. So each cell took TWO suites, one per transport, in the shape the +// temporal cells already had: +// +// FILTER_LOGIC_CASES driven by `turso-filter-logic-conformance` for +// the local transport and by `turso-remote-filter- +// logic-conformance` for the remote one. Both green +// on arrival. +// PAGINATION_CASES, driven by `turso-pagination-conformance` and +// PAGINATION_UNORDERED_CASES `turso-remote-pagination-conformance`, same two +// transports. Also green — but read the note below +// before trusting the remote half of these two the +// way you can trust the local half. +// +// Both remote suites run over the `libsql-sqlite-stub.testkit` SQLite stub, so +// the whole set is hermetic: no network, no credentials, on by default in CI — +// which is what the temporal pair next door already established for this +// package. +// +// ## What driver-turso's PAGINATION cells mean — read this before trusting them +// +// The local suite passes because `SqlDriver.orderKeysFor()` appends the `id` +// tie-breaker the contract asks for. The remote suite passes WITHOUT that +// mechanism: `buildSelectSQL` maps the caller's `orderBy` verbatim and appends +// no unique column, so the cases hold on the stub's twelve-row `better-sqlite3` +// table — one plan, one arrangement, every time — rather than by a promise the +// transport makes. On a real endpoint that arrangement is not promised across +// two statements, which is the defect `pagination-conformance.ts` is about. +// Filed as #5653; the remote suite carries two `records the measured mechanism` +// tests that pin the current no-tie-breaker behaviour so it cannot go quiet +// under a green cell. Not a ledger entry, because the cells ARE covered — an +// entry for a covered cell fails RECONCILED, and this is where the fact fits. +// // An empty ledger is the intended steady state, not a reason to delete the // mechanism: the next driver that arrives uncovered fails CONSUMED and lands // its measured entry here. @@ -191,54 +234,7 @@ const CASE_SETS = [ // investment is frozen (#5499). Un-freezing it is what should re-run these cells // in CI; until then, this note is the honest state of the mongo column. -// ## driver-turso arrived from `objectstack-ai/cloud` with three cells open (#4645) -// -// The package migrated into `packages/drivers/driver-turso` in Phase A of #4645 -// and entered this matrix the moment it landed on disk -- which is the gate -// working as documented ("a new driver package is in scope the moment it -// exists"), not a surprise. Measured on arrival: TEMPORAL_CASES and -// TEMPORAL_TIME_CASES are genuinely covered, twice over -// (`turso-temporal-conformance.test.ts` for the local transport, -// `turso-remote-temporal-conformance.test.ts` for the remote one). The other -// three had no suite in cloud either. -// -// They are DEBT rather than EXEMPT, and the reason is the driver's dual -// transport. Local/replica mode does inherit SqlDriver's filter compiler and -// paging -- but remote mode does not go through Knex at all: -// `src/remote-transport.ts` carries its own `buildWhereSQL` (combinator -// nesting, operator vocabulary, comparand refusal) and its own ORDER BY / -// LIMIT / OFFSET assembly. That is an independent Nth backend, which is -// precisely what #3774 and #4363 wrote the shared case-sets for. "Inherits, -// therefore fine" is the assumption those suites exist to disprove -- the same -// sentence driver-sqlite-wasm's cleared entry carried, and it was cleared by a -// suite, not by the sentence. -// -// Clearing these: write the suites (both transports, hermetic -- the remote -// half over `libsql-sqlite-stub.testkit.ts`), then delete these entries in the -// same PR. Tracked as #5590. -const LEDGER = [ - { - driver: 'driver-turso', - marker: 'FILTER_LOGIC_CASES', - kind: 'DEBT', - why: "remote transport compiles its own WHERE (`src/remote-transport.ts` buildWhereSQL) instead of inheriting SqlDriver's, so combinator nesting is an independent implementation with no suite; local/replica inherits but is untested against the shared cases too.", - issue: '#5590', - }, - { - driver: 'driver-turso', - marker: 'PAGINATION_CASES', - kind: 'DEBT', - why: 'remote transport assembles its own ORDER BY / LIMIT / OFFSET; no suite drives the sorted-partition property against either transport.', - issue: '#5590', - }, - { - driver: 'driver-turso', - marker: 'PAGINATION_UNORDERED_CASES', - kind: 'DEBT', - why: 'same seam as PAGINATION_CASES, unsorted arm: the remote LIMIT/OFFSET path has no suite pinning that an unsorted paged read is still a partition.', - issue: '#5590', - }, -]; +const LEDGER = []; // ── Discovery ───────────────────────────────────────────────────────────────