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