fix(restore): DROP/CREATE DATABASE needs explicit -d maintenance DB

`psql` with no -d connects to a database whose name matches the
connecting user. On installs where the user's home DB doesn't exist
(common pattern: DB_USER=picpeak, DB_NAME=picpeak_prod, no `picpeak`
DB), the restore's DROP DATABASE / CREATE DATABASE statements failed
with:

  FATAL: database "picpeak" does not exist

even though the target DB (picpeak_prod) was alive and connectable.
And of course you can't connect to the target DB itself for DROP —
PostgreSQL refuses while a connection is open to it.

Fix: explicitly connect to `postgres` (the maintenance DB every PG
cluster ships with) for the DROP/CREATE statements. Override via
DB_CHECK_DB env var if the `postgres` DB is restricted to superusers
on the cluster — matches the pattern wait-for-db.sh already exposes.

Also quote the database name in the SQL so installs whose DB has
unusual characters (numbers, hyphens) don't break the statement.

Surfaced during Ralf's end-to-end restore validation — yet another
"never been tested on a real PG install" latent bug exposed by the
Stage A inline-dump path actually being able to produce a restorable
manifest for the first time on his install.
This commit is contained in:
Luca
2026-05-30 13:06:15 +02:00
parent 5c0be66a14
commit 4c31a22626
+26 -6
View File
@@ -828,15 +828,35 @@ class RestoreService {
// PostgreSQL restore
const { host, port, user, password, database } = knexConfig.connection;
const env = { ...process.env, PGPASSWORD: password };
// `psql` with no `-d` defaults to a database whose name matches
// the connecting user, NOT a maintenance DB. So on installs
// where the user's home DB doesn't exist (e.g. user=`picpeak`,
// target DB=`picpeak_prod`, no `picpeak` DB), the next two
// statements failed with:
// FATAL: database "picpeak" does not exist
// even though the actual target DB was alive and connectable.
//
// Fix: explicitly connect to `postgres` (the maintenance DB
// every PG cluster ships with) for the DROP/CREATE. We can't
// connect to the target DB itself anyway — DROP DATABASE
// refuses to run while a connection is open to it.
//
// Use `DB_CHECK_DB` env var as an override hook (matches the
// pattern wait-for-db.sh already exposes) for installs where
// the `postgres` DB is restricted to superusers.
const maintenanceDb = process.env.DB_CHECK_DB || 'postgres';
// Drop and recreate database (extremely dangerous!)
this.log('warn', 'Dropping and recreating PostgreSQL database...');
await spawnAsync('psql', ['-h', host, '-p', String(port), '-U', user, '-c', `DROP DATABASE IF EXISTS ${database}`], { env });
this.log('warn', 'Dropping and recreating PostgreSQL database...', {
target: database, via: maintenanceDb,
});
await spawnAsync('psql', ['-h', host, '-p', String(port), '-U', user, '-c', `CREATE DATABASE ${database}`], { env });
await spawnAsync('psql', ['-h', host, '-p', String(port), '-U', user, '-d', maintenanceDb, '-c', `DROP DATABASE IF EXISTS "${database}"`], { env });
// Restore from backup
await spawnAsync('psql', ['-h', host, '-p', String(port), '-U', user, '-d', maintenanceDb, '-c', `CREATE DATABASE "${database}"`], { env });
// Restore from backup — this one DOES connect to the target DB.
await spawnFromFile('psql', ['-h', host, '-p', String(port), '-U', user, '-d', database], restoreFile, { env });
}