Files
website/scripts/db-migrate.ts
Stan Runge 64fb8df262 db: Drizzle schemas on domain Postgres schemas, baseline migration, reorg SQL
- pgSchema tables for auth, money, house, ref, games, cs, animaldex, site, meta
  (lists excluded via schemaFilter; runtime list tables are managed by meta.*)
- drizzle/ baseline matches the reorged DB exactly (catalog diff = 0 on rehearsal)
- db/reorg: one-time SQL that moves the flat public schema (rehearsed on a prod copy)
- scripts/db-migrate.ts: migrator with --init to record the baseline and a guard
  against running the baseline on a non-empty, unrecorded database
2026-10-23 22:03:42 +00:00

56 lines
2.6 KiB
TypeScript

// Applies pending Drizzle migrations from ./drizzle. Runs as the schema owner
// (DB_MIGRATE_URL, role `personal`); falls back to DB_URL for local dev.
//
// bun scripts/db-migrate.ts apply pending migrations
// bun scripts/db-migrate.ts --init record the baseline as applied without running it
// (once, on a database already reorganised by the
// hand-written reorg scripts in db/reorg/)
import { drizzle } from 'drizzle-orm/postgres-js';
import { migrate } from 'drizzle-orm/postgres-js/migrator';
import postgres from 'postgres';
const url = process.env.DB_MIGRATE_URL || process.env.DB_URL;
if (!url) throw new Error('Set DB_MIGRATE_URL (schema owner) or DB_URL');
const init = process.argv.includes('--init');
const client = postgres(url, { max: 1, connect_timeout: 10, onnotice: () => {} });
try {
const db = drizzle({ client });
// Safety: never run the baseline on a database that already has data. Existing
// databases are brought to the baseline by the reviewed reorg scripts and then
// recorded with --init; only a truly empty database gets the baseline executed.
const [state] = await client`
select
to_regclass('drizzle.__drizzle_migrations') is not null as tracked,
(select count(*)::int from pg_class c join pg_namespace n on n.oid = c.relnamespace
where c.relkind = 'r' and n.nspname not in ('pg_catalog', 'information_schema', 'drizzle')
and n.nspname not like 'pg_toast%') as tables`;
const recorded = state.tracked
? (await client`select count(*)::int as n from drizzle.__drizzle_migrations`)[0].n
: 0;
if (!init && recorded === 0 && state.tables > 0)
throw new Error(
`Refusing to run the Drizzle baseline on a non-empty database (${state.tables} tables). ` +
'Apply the reorg scripts (db/reorg/) and record the baseline with --init first.'
);
if (init && state.tables === 0)
throw new Error('--init on an empty database: run without --init to create the schema');
const result = await migrate(db, {
migrationsFolder: new URL('../drizzle', import.meta.url).pathname,
migrationsSchema: 'drizzle',
migrationsTable: '__drizzle_migrations',
...(init ? { init: true } : {})
});
if (result && 'exitCode' in result) {
throw new Error(
result.exitCode === 'databaseMigrations'
? '--init refused: migrations are already recorded in this database'
: '--init refused: more than one local migration exists'
);
}
const [{ n }] = await client`select count(*)::int as n from drizzle.__drizzle_migrations`;
console.log(`${init ? 'Baseline recorded' : 'Migrations up to date'}; ${n} recorded`);
} finally {
await client.end();
}