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