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([]);