Files
redefined-designs/backend/tests/integration/schemaMirror.integration.test.ts
T
bermudalambandClaude Opus 5 f86ebba046 docs(db): correct the drift test's own stale references (#305)
The #217 doc comment above the describe block still named db-drizzle/schema.ts, drizzle-kit pull, and "Drizzle infers row types" — a file, a command, and a library this same commit had already removed. A comment pointing at deleted paths is worse than no comment at all on a test whose whole job is proving trust in a generated mirror, so it is corrected to name npm run db:types and src/db-kysely/schema.ts while keeping every sentence of the history intact: #217, #222, item_drafts and upload_links, the week nobody noticed. A closing note was added recording that the generator changed in #305 and the test did not, because the drift it guards is a property of generating a mirror at all rather than of any particular library.

mirroredTables' regex also gets the same digit fix the column check already had. Both regexes parse the same generated file for the same kind of identifier, and a table name with a digit would otherwise be read out of the DB interface but reported missing by mirroredTables, sending someone to regenerate a file that was never wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 15:11:11 -05:00

108 lines
4.1 KiB
TypeScript

import { readFileSync } from 'fs';
import path from 'path';
import { pool } from '../../src/db';
import { closeDb } from './setup/testDb';
afterAll(async () => {
await pool.end();
await closeDb();
});
const SCHEMA = readFileSync(
path.join(__dirname, '..', '..', 'src', 'db-kysely', 'schema.ts'),
'utf8'
);
/**
* Every table name the generated mirror declares.
*
* 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.
*/
function mirroredTables(): string[] {
const block = /export interface DB \{([^}]*)\}/.exec(SCHEMA)?.[1] ?? '';
return [...block.matchAll(/^\s*([a-z0-9_]+):/gm)].map((m) => m[1]!).sort();
}
async function liveTables(): Promise<string[]> {
const { rows } = await pool.query<{ table_name: string }>(
`SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
AND table_name <> 'pgmigrations'
ORDER BY table_name`
);
return rows.map((r) => r.table_name);
}
/**
* The guard for #217.
*
* `src/db-kysely/schema.ts` is generated by `npm run db:types` and is a
* read-only mirror of the real schema, which `backend/migrations` owns. Nothing
* makes anyone regenerate after writing a migration, and that is not
* hypothetical: the mirror sat missing `item_drafts` and `upload_links` from
* the moment #222 landed until #217, because it had been copied into src/ by
* hand and nobody had reason to look at it.
*
* A stale mirror is worse than no mirror. Row types are inferred from it, so a
* converted query would type-check against a schema the database does not have
* and fail at run time with a column that does not exist — the exact class of
* drift the adoption was meant to close.
*
* The generator changed in #305 and this test did not, because the drift it
* guards is a property of generating a mirror at all rather than of any
* library.
*/
describe('the generated schema mirror', () => {
it('declares every table the migrations create', async () => {
const live = await liveTables();
const mirrored = mirroredTables();
const missing = live.filter((name) => !mirrored.includes(name));
expect(missing).toEqual([]);
});
it('declares no table the database does not have', async () => {
const live = await liveTables();
const mirrored = mirroredTables();
const extra = mirrored.filter((name) => !live.includes(name));
expect(extra).toEqual([]);
});
// pgmigrations is node-pg-migrate's bookkeeping, excluded by
// --exclude-pattern in the db:types script. A regeneration without that flag
// would quietly put it back.
it('excludes node-pg-migrate bookkeeping', () => {
expect(mirroredTables()).not.toContain('pgmigrations');
});
// Columns, not just tables: a migration that adds a column to a table the
// 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.
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
WHERE table_schema = 'public' AND table_name <> 'pgmigrations'
ORDER BY table_name, column_name`
);
const declared = new Set(
[...SCHEMA.matchAll(/^\s{2}([a-z0-9_]+):/gm)].map((m) => m[1]!)
);
const missing = rows
.filter((row) => !declared.has(row.column_name))
.map((row) => `${row.table_name}.${row.column_name}`);
expect(missing).toEqual([]);
});
});