Feature/305 kysely swap #306

Merged
bermudalamb merged 8 commits from feature/305-kysely-swap into main 2026-09-04 16:08:50 -05:00
3 changed files with 40 additions and 32 deletions
Showing only changes of commit 2670a09b84 - Show all commits
-9
View File
@@ -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/
+6 -7
View File
@@ -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'
]
},
@@ -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<string, Set<string>> {
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[]> {
@@ -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([]);