fix(restore): re-sync PostgreSQL sequences after psql load
pg_dump emits setval() statements for SERIAL/IDENTITY columns, but
they don't always land cleanly: --clean ordering, knex pool sequence
caching, rows inserted mid-restore (the pre-restore safety backup
writes a database_backup_runs row before DROP), etc. Net result on
Ralf's install after a successful restore:
- "A record with this value already exists" on every CRUD action
- duplicate key value violates unique constraint
"database_backup_runs_pkey" on the next Run Backup Now
Same root cause: every SERIAL column's sequence was pointing at or
below MAX(id), so the next INSERT collided.
Fix: append a DO block after the psql restore that walks pg_class +
pg_attribute and setval()s every public-schema sequence to
GREATEST(MAX(<col>), 1). Cheap (a few ms even on large schemas),
safe (read-only on row data), idempotent — re-running it just
re-asserts the same values.
Seventh latent PG-restore bug discovered on Ralf's install tonight.
Manual hand-fix worked; this commit makes the fix automatic for
every future restore.
This commit is contained in:
@@ -906,6 +906,52 @@ class RestoreService {
|
||||
|
||||
// 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 });
|
||||
|
||||
// Re-sync every SERIAL / IDENTITY sequence in the public schema
|
||||
// to MAX(id)+1 of its owning table. pg_dump emits setval()
|
||||
// statements, but they don't always land cleanly when:
|
||||
// - the dump has `--clean` (the setval may execute before
|
||||
// the rebuilt rows, depending on dump ordering)
|
||||
// - the in-process knex pool had a cached sequence value
|
||||
// before db.destroy() (already mitigated, but defensive)
|
||||
// - rows were inserted mid-restore (the pre-restore safety
|
||||
// backup creates a database_backup_runs row before DROP)
|
||||
// Result if skipped: every subsequent INSERT into a serial-id
|
||||
// table fails with `duplicate key value violates unique
|
||||
// constraint "<table>_pkey"`. Surfaced on Ralf's install as
|
||||
// "A record with this value already exists" on every CRUD
|
||||
// action AND `database_backup_runs_pkey` violation on the
|
||||
// next Run Backup Now. Fix is a single DO block that walks
|
||||
// pg_class + pg_attribute and setval()s each sequence to
|
||||
// GREATEST(MAX(<col>), 1). Cheap (a few ms even on large
|
||||
// schemas), safe (doesn't touch row data), idempotent.
|
||||
this.log('info', 'Re-syncing PostgreSQL sequences to MAX(id) of each table...');
|
||||
await spawnAsync('psql', [
|
||||
'-h', host, '-p', String(port), '-U', user, '-d', database,
|
||||
'-c',
|
||||
`DO $$
|
||||
DECLARE
|
||||
r RECORD;
|
||||
max_id BIGINT;
|
||||
BEGIN
|
||||
FOR r IN
|
||||
SELECT n.nspname AS schema_name, t.relname AS table_name, a.attname AS column_name,
|
||||
pg_get_serial_sequence(quote_ident(n.nspname) || '.' || quote_ident(t.relname), a.attname) AS seq_name
|
||||
FROM pg_class t
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
JOIN pg_attribute a ON a.attrelid = t.oid
|
||||
WHERE n.nspname = 'public'
|
||||
AND t.relkind = 'r'
|
||||
AND a.attnum > 0
|
||||
AND NOT a.attisdropped
|
||||
AND pg_get_serial_sequence(quote_ident(n.nspname) || '.' || quote_ident(t.relname), a.attname) IS NOT NULL
|
||||
LOOP
|
||||
EXECUTE format('SELECT COALESCE(MAX(%I), 0) FROM %I.%I', r.column_name, r.schema_name, r.table_name) INTO max_id;
|
||||
EXECUTE format('SELECT setval(%L, %s, true)', r.seq_name, GREATEST(max_id, 1));
|
||||
END LOOP;
|
||||
END $$;`
|
||||
], { env });
|
||||
this.log('info', 'Sequence resync completed');
|
||||
}
|
||||
|
||||
// Re-initialize database connection
|
||||
|
||||
Reference in New Issue
Block a user