- 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
56 lines
2.6 KiB
TypeScript
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();
|
|
}
|