Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions control-plane/src/cutover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,15 @@ export async function stageA(name: string): Promise<void> {
if (!row.db_password) {
throw new Error(`project '${name}' has no stored db password — re-provision it`);
}
// stageA assumes the app is serving from pg16: it (re)builds a fresh v17 copy
// from the current pg16 data. Refuse if the project is ALREADY on pg17 — its
// v17 db is live, and re-running would drop it and overwrite with a stale pg16
// snapshot. Roll back to pg16 first if a re-run is really intended.
if (row.cluster === "pg17") {
throw new Error(
`project '${name}' is already bound to pg17 — stageA would overwrite its live v17 db with a stale pg16 copy. Roll back first (cutover --rollback) if you mean to re-run.`,
);
}
const password = row.db_password;

const src = connOf(config.adminUrl);
Expand Down Expand Up @@ -186,13 +195,15 @@ export async function stageA(name: string): Promise<void> {
`alter role ${ident(role)} set idle_in_transaction_session_timeout = '60s'`,
);

// 3. The database on v17 (from template0 — no supabase seed conflicts with
// the restore) + connect privileges scoped to this project's role only.
console.log(`[cutover ${name}] stageA 3/7 database ${database}`);
const d = await v17admin.query("select 1 from pg_database where datname = $1", [database]);
if (!d.rowCount) {
await v17admin.query(`create database ${ident(database)} template template0`);
}
// 3. The database on v17, ALWAYS from a clean slate (from template0 — no
// supabase seed conflicts with the restore). Dropping first makes stageA
// idempotent: a leftover copy (an earlier stageA, or a hand-staged dump)
// would otherwise make the pg_restore below fail on duplicate objects. The
// project is on pg16 here (guarded above), so nothing live depends on the
// v17 copy — WITH (FORCE) just clears any idle leftover connection.
console.log(`[cutover ${name}] stageA 3/7 (re)create database ${database} on pg17`);
await v17admin.query(`drop database if exists ${ident(database)} with (force)`);
await v17admin.query(`create database ${ident(database)} template template0`);
await v17admin.query(`revoke connect on database ${ident(database)} from public`);
await v17admin.query(`grant connect on database ${ident(database)} to ${ident(role)}`);
} finally {
Expand Down
Loading