mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
123 lines
4.6 KiB
TypeScript
123 lines
4.6 KiB
TypeScript
import { runMigrations } from '../../../src/db/migrations';
|
|
import { createTables } from '../../../src/db/schema';
|
|
import { readSchemaSnapshot } from '../../../src/db/schema-snapshot';
|
|
|
|
import Database from 'better-sqlite3';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
/**
|
|
* The app builds its schema from the MikroORM migrations in `src/db/migrations/`.
|
|
* 182 test files still build theirs from `schema.ts` + `migrations.ts`, because
|
|
* converting them was deliberately left out of the ORM switch.
|
|
*
|
|
* That is two sources for one schema, which the root CLAUDE.md allows only
|
|
* behind "a parity test that cannot silently skip". This is that test: if either
|
|
* side gains a column, an index, a constraint or a table the other does not
|
|
* have, it fails and names the difference.
|
|
*
|
|
* Scope is the schema, not the rows. `tests/helpers/test-db.ts::seedDefaults` is
|
|
* already a deliberate fork of production seeding (8 addons against 14, its own
|
|
* names, no admin user), so asserting on seeded data would fail for reasons that
|
|
* have nothing to do with schema drift.
|
|
*/
|
|
|
|
interface ColumnRow {
|
|
name: string;
|
|
type: string;
|
|
notnull: number;
|
|
dflt_value: string | null;
|
|
pk: number;
|
|
}
|
|
interface IndexRow {
|
|
name: string;
|
|
unique: number;
|
|
partial: number;
|
|
}
|
|
interface ForeignKeyRow {
|
|
table: string;
|
|
from: string;
|
|
to: string | null;
|
|
on_update: string;
|
|
on_delete: string;
|
|
}
|
|
|
|
/** Everything about a database's shape that a drift would show up in. */
|
|
function describeSchema(db: Database.Database): Record<string, string[]> {
|
|
const tables = (
|
|
db
|
|
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name`)
|
|
.all() as { name: string }[]
|
|
).map((r) => r.name);
|
|
|
|
const out: Record<string, string[]> = {};
|
|
for (const table of tables) {
|
|
const columns = (db.prepare(`PRAGMA table_info('${table}')`).all() as ColumnRow[])
|
|
.map((c) => `column ${c.name} ${c.type.toUpperCase()} notnull=${c.notnull} default=${c.dflt_value} pk=${c.pk}`)
|
|
.sort();
|
|
|
|
const indexes = (db.prepare(`PRAGMA index_list('${table}')`).all() as IndexRow[])
|
|
.map((index) => {
|
|
const on = (db.prepare(`PRAGMA index_info('${index.name}')`).all() as { name: string }[]).map((c) => c.name);
|
|
// An inline UNIQUE(...) becomes an auto-named index, so compare those by
|
|
// their columns rather than by a name SQLite invented.
|
|
return index.name.startsWith('sqlite_autoindex')
|
|
? `unique constraint (${on.join(', ')})`
|
|
: `index ${index.name} unique=${index.unique} partial=${index.partial} (${on.join(', ')})`;
|
|
})
|
|
.sort();
|
|
|
|
const foreignKeys = (db.prepare(`PRAGMA foreign_key_list('${table}')`).all() as ForeignKeyRow[])
|
|
.map((fk) => `foreign key ${fk.from} -> ${fk.table}(${fk.to}) on delete ${fk.on_delete}`)
|
|
.sort();
|
|
|
|
out[table] = [...columns, ...indexes, ...foreignKeys];
|
|
}
|
|
|
|
for (const kind of ['trigger', 'view'] as const) {
|
|
const rows = db.prepare(`SELECT name, sql FROM sqlite_master WHERE type = ? ORDER BY name`).all(kind) as {
|
|
name: string;
|
|
sql: string | null;
|
|
}[];
|
|
out[`(${kind}s)`] = rows.map((r) => `${r.name}: ${r.sql?.replace(/\s+/g, ' ').trim()}`);
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
describe('schema parity between the legacy builders and the MikroORM migrations', () => {
|
|
it('SCHEMA-001: db/schema.ts + db/migrations.ts produce the same shape as src/db/migrations/', () => {
|
|
const snapshot = readSchemaSnapshot();
|
|
// The snapshot is built by tests/global-setup.ts, which runs for every
|
|
// `vitest run`. Failing loudly rather than skipping is the point: a silent
|
|
// skip here is exactly the hole this test exists to close.
|
|
expect(
|
|
snapshot,
|
|
'no schema snapshot — tests/global-setup.ts did not run, so migration parity was never checked',
|
|
).not.toBeNull();
|
|
|
|
const migrated = new Database(snapshot!);
|
|
|
|
const legacy = new Database(':memory:');
|
|
legacy.exec('PRAGMA foreign_keys = ON');
|
|
createTables(legacy);
|
|
runMigrations(legacy);
|
|
|
|
try {
|
|
const fromMigrations = describeSchema(migrated);
|
|
const fromLegacy = describeSchema(legacy);
|
|
|
|
// The migrator's own bookkeeping only exists on the MikroORM side, and the
|
|
// legacy chain has no equivalent to compare it against.
|
|
delete fromMigrations.mikro_orm_migrations;
|
|
|
|
expect(Object.keys(fromMigrations).sort()).toEqual(Object.keys(fromLegacy).sort());
|
|
for (const table of Object.keys(fromLegacy)) {
|
|
expect(fromMigrations[table], `table "${table}" differs`).toEqual(fromLegacy[table]);
|
|
}
|
|
} finally {
|
|
migrated.close();
|
|
legacy.close();
|
|
}
|
|
});
|
|
});
|