133 lines
3.5 KiB
TypeScript
133 lines
3.5 KiB
TypeScript
import pg from "pg";
|
|
import { run } from "../helpers/spawn.ts";
|
|
import { REPO_ROOT } from "../helpers/paths.ts";
|
|
import { type Env, targetHost, wrapInInfisical } from "../helpers/env.ts";
|
|
import {
|
|
getPendingMigrations,
|
|
type PendingMigration,
|
|
} from "../helpers/pendingMigrations.ts";
|
|
import {
|
|
type BlockingStatement,
|
|
findBlockingIndexStatements,
|
|
} from "../helpers/safetyCheck.ts";
|
|
|
|
export async function cmdMigrate(
|
|
env: Env,
|
|
opts: { dryRun: boolean; bootstrap: boolean },
|
|
): Promise<void> {
|
|
await wrapInInfisical(env);
|
|
|
|
const databaseUrl = process.env.DATABASE_URL;
|
|
if (!databaseUrl) {
|
|
console.error("DATABASE_URL not set after infisical wrap");
|
|
process.exit(1);
|
|
}
|
|
|
|
const tags = [
|
|
opts.dryRun ? "DRY" : null,
|
|
opts.bootstrap ? "BOOTSTRAP" : null,
|
|
].filter(Boolean);
|
|
const tagStr = tags.length > 0 ? ` (${tags.join(" ")})` : "";
|
|
console.log(`[db:migrate${tagStr}] env=${env} host=${targetHost(databaseUrl)}`);
|
|
|
|
if (opts.bootstrap) {
|
|
console.log(
|
|
"bootstrap mode: skipping index-DDL safety check (use only for fresh DBs)",
|
|
);
|
|
}
|
|
|
|
const client = new pg.Client({ connectionString: databaseUrl });
|
|
await client.connect();
|
|
let pending: PendingMigration[];
|
|
try {
|
|
pending = await getPendingMigrations(client);
|
|
} finally {
|
|
await client.end();
|
|
}
|
|
|
|
if (pending.length === 0) {
|
|
console.log("no pending migrations");
|
|
return;
|
|
}
|
|
|
|
console.log(`${pending.length} pending migration(s):`);
|
|
for (const migration of pending) {
|
|
console.log(` ${migration.tag}`);
|
|
}
|
|
|
|
const blockers = opts.bootstrap ? [] : collectBlockers(pending);
|
|
|
|
if (opts.dryRun) {
|
|
console.log("");
|
|
console.log("--- SQL preview ---");
|
|
for (const migration of pending) {
|
|
console.log("");
|
|
console.log(`-- ${migration.tag}.sql --`);
|
|
console.log(migration.sql);
|
|
}
|
|
console.log("");
|
|
if (blockers.length > 0) {
|
|
printBlockerError(blockers);
|
|
console.log("dry-run: would refuse to apply.");
|
|
process.exit(1);
|
|
}
|
|
console.log(
|
|
`dry-run: ok — \`bun db migrate --env=${env}\` would apply these.`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (blockers.length > 0) {
|
|
printBlockerError(blockers);
|
|
process.exit(1);
|
|
}
|
|
|
|
const { code } = await run("bun", ["-F", "@autumn/shared", "db:migrate"], {
|
|
cwd: REPO_ROOT,
|
|
});
|
|
process.exit(code);
|
|
}
|
|
|
|
type FlaggedBlocker = {
|
|
migration: PendingMigration;
|
|
blocker: BlockingStatement;
|
|
};
|
|
|
|
function collectBlockers(pending: PendingMigration[]): FlaggedBlocker[] {
|
|
const all: FlaggedBlocker[] = [];
|
|
for (const migration of pending) {
|
|
for (const blocker of findBlockingIndexStatements(migration.sql)) {
|
|
all.push({ migration, blocker });
|
|
}
|
|
}
|
|
return all;
|
|
}
|
|
|
|
function printBlockerError(blockers: FlaggedBlocker[]): void {
|
|
console.error("");
|
|
console.error(
|
|
`refusing to apply ${blockers.length} index DDL statement(s) without CONCURRENTLY:`,
|
|
);
|
|
for (const { migration, blocker } of blockers) {
|
|
console.error("");
|
|
console.error(` in ${migration.tag}.sql [${blocker.kind}]`);
|
|
const indented = blocker.statement
|
|
.split("\n")
|
|
.map((line) => ` ${line}`)
|
|
.join("\n");
|
|
console.error(indented);
|
|
}
|
|
console.error("");
|
|
console.error(
|
|
"Index DDL without CONCURRENTLY takes an ACCESS EXCLUSIVE lock that blocks reads/writes on the table.",
|
|
);
|
|
console.error("");
|
|
console.error("Options:");
|
|
console.error(
|
|
" 1. Apply this migration manually (psql / TablePlus) with CONCURRENTLY, then `bun db mark-applied --env=<env>` to record it.",
|
|
);
|
|
console.error(
|
|
" 2. If the table is fresh or empty, add `.concurrently()` to the index in shared/db schema, `bun db generate`, and re-try.",
|
|
);
|
|
}
|