fix(db): repoint the lint and drift guards at the new mirror (#305)

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 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 16:08:47 -05:00
co-authored by Claude Opus 5
parent ad5cb28e80
commit add28c5f16
3 changed files with 40 additions and 32 deletions
-9
View File
@@ -21,15 +21,6 @@ backend/unit-results.json
backend/integration-results.json backend/integration-results.json
frontend/playwright-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. # Where an end-to-end run against the throwaway database writes its uploads.
# Disposable with the database it belongs to (#186). # Disposable with the database it belongs to (#186).
backend/.e2e-uploads/ backend/.e2e-uploads/
+6 -7
View File
@@ -30,18 +30,17 @@ const advisory = (config) => ({
}); });
export default tseslint.config( export default tseslint.config(
// src/db-drizzle/schema.ts and relations.ts are `drizzle-kit pull` output, not // src/db-kysely/schema.ts is `kysely-codegen` output, not written by anyone
// written by anyone here. #261 hand-fixed an unused-parameter warning in the // here. #261 hand-fixed an unused-parameter warning in the equivalent Drizzle
// schema and #217's re-pull put it straight back, which is the whole argument: // file and #217's regeneration put it straight back, which is the whole
// linting generated code buys a fix that the next regeneration undoes. The // argument: linting generated code buys a fix that the next regeneration
// hand-written files in that directory are still linted. // undoes. The hand-written files in that directory are still linted.
{ {
ignores: [ ignores: [
'dist/**', 'dist/**',
'coverage/**', 'coverage/**',
'eslint.config.mjs', 'eslint.config.mjs',
'src/db-drizzle/schema.ts', 'src/db-kysely/schema.ts'
'src/db-drizzle/relations.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 * The `DB` interface maps a table name to the interface that declares that
* them all, mapping the database's own snake_case name to the interface for * table's columns, so the two have to be read together. A flat set of every
* that table. Parsing the file as text rather than importing it is deliberate * column name appearing anywhere in the file is not the same assertion and is
* and unchanged from the Drizzle version: these are TypeScript types, erased at * far weaker than it looks: seventeen column names are declared on more than
* run time, so there is nothing to import and inspect. * 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<string, Set<string>> {
const block = /export interface DB \{([^}]*)\}/.exec(SCHEMA)?.[1] ?? ''; 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<string, Set<string>>();
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<string[]> { async function liveTables(): Promise<string[]> {
@@ -84,10 +103,11 @@ describe('the generated schema mirror', () => {
// mirror already knows about is the likelier drift, and the one a table-level // mirror already knows about is the likelier drift, and the one a table-level
// check would wave through. // check would wave through.
// //
// One exact match, where the Drizzle version needed two and called itself // One exact match per table, where the Drizzle version needed two matches and
// "deliberately loose" for it. kysely-codegen emits the database's own name // no table at all, and called itself "deliberately loose" for it.
// as a bare interface key, so ` column_name:` at the start of a line is the // kysely-codegen emits the database's own name as a bare interface key and
// whole of the naming rule and there is nothing to re-implement. // 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 () => { it('declares every column of every table it mirrors', async () => {
const { rows } = await pool.query<{ table_name: string; column_name: string }>( const { rows } = await pool.query<{ table_name: string; column_name: string }>(
`SELECT table_name, column_name FROM information_schema.columns `SELECT table_name, column_name FROM information_schema.columns
@@ -95,11 +115,9 @@ describe('the generated schema mirror', () => {
ORDER BY table_name, column_name` ORDER BY table_name, column_name`
); );
const declared = new Set( const columns = mirroredColumns();
[...SCHEMA.matchAll(/^\s{2}([a-z0-9_]+):/gm)].map((m) => m[1]!)
);
const missing = rows 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}`); .map((row) => `${row.table_name}.${row.column_name}`);
expect(missing).toEqual([]); expect(missing).toEqual([]);