From cfaa7eb095514997ca9033c1164ff0091b0322dc Mon Sep 17 00:00:00 2001
From: Luca <102960244+Luca-Timo@users.noreply.github.com>
Date: Sat, 30 May 2026 21:10:10 +0200
Subject: [PATCH] fix(restore): re-sync PostgreSQL sequences after psql load
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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(
), 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.
---
backend/src/services/restoreService.js | 46 ++++++++++++++++++++++++++
1 file changed, 46 insertions(+)
diff --git a/backend/src/services/restoreService.js b/backend/src/services/restoreService.js
index 1ea6c702..a17ecc52 100644
--- a/backend/src/services/restoreService.js
+++ b/backend/src/services/restoreService.js
@@ -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 "_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(), 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