From 2670a09b84ea161d54be238a3c0f8ba02461eefa Mon Sep 17 00:00:00 2001 From: Thom Lamb Date: Fri, 4 Sep 2026 15:31:35 -0500 Subject: [PATCH] fix(db): repoint the lint and drift guards at the new mirror (#305) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The eslint config's ignores list and comment still named the deleted src/db-drizzle/schema.ts and relations.ts and never named src/db-kysely/schema.ts, so the generated mirror was being linted for the first time and tripping sonarjs/redundant-type-aliases — exactly the trap the config's own comment already described from #261 and #217. The ignores list now names src/db-kysely/schema.ts and the comment is updated to match. The schema mirror drift test built one flat Set of every two-space-indented key in the whole generated file and asked only whether a live column name appeared anywhere in it, rather than checking it against the specific table it belongs to. Seventeen column names are declared on two or more tables and created_at is on fourteen of eighteen, so a migration adding created_at, updated_at, status, name, sort_order, token, or expires_at to a table that lacks it would pass vacuously. Replaced mirroredTables with mirroredColumns, which reads the DB interface to map each table name to its declaring interface and then reads that interface's own columns, and changed the column-mirroring test to look up columns per table. Verified the guard can actually fail: removing customer_id from the Carts interface made the test fail naming carts.customer_id exactly, and restoring the file made it pass again. The root .gitignore still carried a comment block and two patterns for drizzle-kit pull output under backend/src/db-drizzle, a directory this branch deleted along with backend/drizzle.config.ts. kysely-codegen writes only the single tracked file it's pointed at, so nothing replaces the rule — deleted the block and both patterns. Co-Authored-By: Claude Opus 5 --- .gitignore | 9 ---- backend/eslint.config.mjs | 13 +++-- .../schemaMirror.integration.test.ts | 50 +++++++++++++------ 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index 8525073..603b7ba 100755 --- a/.gitignore +++ b/.gitignore @@ -21,15 +21,6 @@ backend/unit-results.json backend/integration-results.json frontend/playwright-results.json -# drizzle-kit pull writes the schema mirror into backend/src/db-drizzle (see -# backend/drizzle.config.ts), but `out` is also where it would put generated -# migrations and their journal. This project's migration history is -# backend/migrations — hand-written, and mostly prose. #219 has not chosen -# otherwise, so a stray 0000_*.sql in src/ is at best noise and at worst -# mistaken for real migration history. Keep the mirror, drop the rest. -backend/src/db-drizzle/*.sql -backend/src/db-drizzle/meta/ - # Where an end-to-end run against the throwaway database writes its uploads. # Disposable with the database it belongs to (#186). backend/.e2e-uploads/ diff --git a/backend/eslint.config.mjs b/backend/eslint.config.mjs index 07af853..3e488d4 100644 --- a/backend/eslint.config.mjs +++ b/backend/eslint.config.mjs @@ -30,18 +30,17 @@ const advisory = (config) => ({ }); export default tseslint.config( - // src/db-drizzle/schema.ts and relations.ts are `drizzle-kit pull` output, not - // written by anyone here. #261 hand-fixed an unused-parameter warning in the - // schema and #217's re-pull put it straight back, which is the whole argument: - // linting generated code buys a fix that the next regeneration undoes. The - // hand-written files in that directory are still linted. + // src/db-kysely/schema.ts is `kysely-codegen` output, not written by anyone + // here. #261 hand-fixed an unused-parameter warning in the equivalent Drizzle + // file and #217's regeneration put it straight back, which is the whole + // argument: linting generated code buys a fix that the next regeneration + // undoes. The hand-written files in that directory are still linted. { ignores: [ 'dist/**', 'coverage/**', 'eslint.config.mjs', - 'src/db-drizzle/schema.ts', - 'src/db-drizzle/relations.ts' + 'src/db-kysely/schema.ts' ] }, diff --git a/backend/tests/integration/schemaMirror.integration.test.ts b/backend/tests/integration/schemaMirror.integration.test.ts index 2ccc7d1..934a29f 100644 --- a/backend/tests/integration/schemaMirror.integration.test.ts +++ b/backend/tests/integration/schemaMirror.integration.test.ts @@ -14,17 +14,36 @@ const SCHEMA = readFileSync( ); /** - * Every table name the generated mirror declares. + * Every column the mirror declares, keyed by the database's own table name. * - * Read from the `DB` interface, which is the one place kysely-codegen lists - * them all, mapping the database's own snake_case name to the interface for - * that table. Parsing the file as text rather than importing it is deliberate - * and unchanged from the Drizzle version: these are TypeScript types, erased at - * run time, so there is nothing to import and inspect. + * The `DB` interface maps a table name to the interface that declares that + * table's columns, so the two have to be read together. A flat set of every + * column name appearing anywhere in the file is not the same assertion and is + * far weaker than it looks: seventeen column names are declared on more than + * one table and `created_at` is on fourteen of the eighteen, so a migration + * adding one of those to a table that lacks it would pass without the mirror + * knowing anything about it. + * + * Parsed as text rather than imported, because these are TypeScript types and + * are erased at run time — there is nothing to import and inspect. */ -function mirroredTables(): string[] { +function mirroredColumns(): Map> { const block = /export interface DB \{([^}]*)\}/.exec(SCHEMA)?.[1] ?? ''; - return [...block.matchAll(/^\s*([a-z0-9_]+):/gm)].map((m) => m[1]!).sort(); + const byTable = new Map>(); + for (const [, table, iface] of block.matchAll(/^\s*([a-z0-9_]+):\s*([A-Za-z0-9_]+);/gm)) { + const declaration = + new RegExp(`export interface ${iface!} \\{([^}]*)\\}`).exec(SCHEMA)?.[1] ?? ''; + byTable.set( + table!, + new Set([...declaration.matchAll(/^\s{2}([a-z0-9_]+):/gm)].map((m) => m[1]!)) + ); + } + return byTable; +} + +/** Every table name the generated mirror declares. */ +function mirroredTables(): string[] { + return [...mirroredColumns().keys()].sort(); } async function liveTables(): Promise { @@ -84,10 +103,11 @@ describe('the generated schema mirror', () => { // mirror already knows about is the likelier drift, and the one a table-level // check would wave through. // - // One exact match, where the Drizzle version needed two and called itself - // "deliberately loose" for it. kysely-codegen emits the database's own name - // as a bare interface key, so ` column_name:` at the start of a line is the - // whole of the naming rule and there is nothing to re-implement. + // One exact match per table, where the Drizzle version needed two matches and + // no table at all, and called itself "deliberately loose" for it. + // kysely-codegen emits the database's own name as a bare interface key and + // the DB interface says which interface belongs to which table, so the pair + // is the whole of the naming rule and there is nothing to re-implement. it('declares every column of every table it mirrors', async () => { const { rows } = await pool.query<{ table_name: string; column_name: string }>( `SELECT table_name, column_name FROM information_schema.columns @@ -95,11 +115,9 @@ describe('the generated schema mirror', () => { ORDER BY table_name, column_name` ); - const declared = new Set( - [...SCHEMA.matchAll(/^\s{2}([a-z0-9_]+):/gm)].map((m) => m[1]!) - ); + const columns = mirroredColumns(); const missing = rows - .filter((row) => !declared.has(row.column_name)) + .filter((row) => !columns.get(row.table_name)?.has(row.column_name)) .map((row) => `${row.table_name}.${row.column_name}`); expect(missing).toEqual([]);