mirror of
https://github.com/kevinveenbirkenbach/docker-volume-backup.git
synced 2026-08-20 21:22:54 +00:00
fix(restore): refuse a cluster restore that would destroy what it cannot restore
The --empty pre-clean is a catalog-wide sweep: it drops every non-template database and every non-pg_ role of the instance. On a dedicated instance that is exactly right, because the dump recreates all of it. On a shared one it destroys databases the dump does not carry, with nothing to restore them from - and no test ever executed that sweep, because the e2e dropped the cluster by hand first and left the pre-clean with zero rows to generate. Scoping the sweep to the dump's own inventory looks like the fix and is worse. A surviving database that owns or merely grants to one of the dump's roles pins that role in pg_shdepend; DROP OWNED BY only reaches the control database the pre-clean is connected to, so DROP ROLE fails - after phase 1 has already dropped the dump's databases. ON_ERROR_STOP aborts, the replay never starts, and the instance is left half emptied. So the instance is checked instead. --empty now refuses when the instance holds a database the dump does not carry, names it, and touches nothing. The sweep stays as it was, safe behind that refusal. Reading the dump's inventory needs a real identifier parser: a quoted name may hold spaces, and psql options precede the target of a \\connect line. The e2e no longer drops the cluster itself, so --empty has to do it and the replay has to put it back; a second pass then adds a foreign database and requires the refusal to leave both it and the restored data alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,8 @@ class TestClusterReplay(unittest.TestCase):
|
||||
calls = []
|
||||
|
||||
def _capture(container, argv, **kwargs):
|
||||
if "-tAc" in argv:
|
||||
return MagicMock(stdout=b"")
|
||||
calls.append((argv, kwargs.get("stdin")))
|
||||
return MagicMock()
|
||||
|
||||
@@ -58,7 +60,7 @@ class TestClusterReplay(unittest.TestCase):
|
||||
self.assertIn("DROP OWNED BY", preclean)
|
||||
self.assertIn("ORDER BY phase", preclean)
|
||||
|
||||
def test_the_preclean_spares_what_the_dump_does_not_recreate(self) -> None:
|
||||
def test_the_preclean_spares_what_no_dump_recreates(self) -> None:
|
||||
preclean = self._replay(empty=True)[0][1].decode()
|
||||
self.assertIn("NOT datistemplate", preclean)
|
||||
self.assertIn("datname <> current_database()", preclean)
|
||||
|
||||
Reference in New Issue
Block a user