Restore on PostgreSQL: pg_restore --clean over a newer schema can fail half-way and leave the database broken #477
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#477
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, reading the PostgreSQL half ofcore/backup.pyagainst pg_restore's documented behaviour (not executed: no PostgreSQL server was available for this review).Checked again on 2026-10-01 against
mainat0a5cad587: still present.The wiki promises that a restore "runs migrations so that a backup from an older Postulo lands correctly on a newer one". On SQLite that holds, because the backup API replaces the whole file. On PostgreSQL,
load_databaserunspg_restore --clean --if-existsinto the existing database, without--single-transactionor--exit-on-error.--cleandrops only the objects the dump contains, so tables and foreign keys created by migrations newer than the archive stay behind; pg_restore carries on past every failing statement and reports a non-zero exit only at the end, by which time the schema has been partly rewritten.core/backup.py:234load_database(), the argument list atcore/backup.py:258-267;core/backup.py:565restore_backup()then runsmigrate.auth_user, for example). This is also the state of a freshly installed instance, which the entry point has migrated before the documented container restore runs.DROP TABLEon a table the newer table references fails, the matchingCREATE TABLEthen fails because the table is still there, and data is copied into whatever survived, failing wherever a key already exists (on a fresh instance,django_migrations,django_content_typeandauth_permissionalready hold rows).restore_backupraisesBackupError, and the database is neither the old one nor the archive: some tables replaced, some not, content types possibly from the fresh instance rather than the archive, and adjango_migrationstable that no longer matches the schema, so the nextmigratecan fail on "relation already exists".publicschema (or drop every table Django's introspection lists, withCASCADE), so nothing newer than the dump remains to block it.pg_restore --single-transaction --exit-on-error(the first implies the second), so a failing statement rolls the whole load back.Related to #219 (closed), which first exercised the PostgreSQL restore in CI, and #234 (closed), which settled the restore procedure.
Done when
migrate.tests/test_backup.py: a test that a pg_restore failure leaves the database as it was (the new database is dropped, or the transaction is rolled back).