A finished generation cannot show whether a volume held a database, nor whether a dump was produced for it: under --only-sql a failed dump falls back to a file copy, and the resulting files/ tree looks like any other copy. The run knows both and threw the knowledge away as a printed warning, leaving every reader to guess from file names.
Each generation now carries a manifest.json stating its layout and, per volume, database / dumped / engine. baudolo.generation is the single place those names are spelled; restore/paths.py, backup/db.py and backup/volume.py stop repeating them. It is deliberately import-free so a consumer can read the manifest with nothing but json, on hosts where this package is not installed.
BREAKING CHANGE: BackupException is renamed BackupError. The rename is atomic across the ten modules that define or import it, three of which also carry the manifest change, so it lands in this commit rather than a separate one that could not import.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A databases.csv row asking for every database of an instance
(database = '*') makes the backup side write <instance>.cluster.backup.sql
via pg_dumpall, and nothing could read it back: the restore CLI knew
files, postgres and mariadb. That dump was stored and unrestorable - a
format whose producer had no consumer.
Adds `baudolo-restore cluster`. Three properties of a cluster stream
shape it, and each one bit during development:
- It recreates databases, and CREATE DATABASE cannot run inside a
transaction block. So unlike the single-database replay this one must
NOT be wrapped in --single-transaction. The unit tests now pin both
contracts against each other.
- It recreates every role including the one the replay connects as, and
the pre-clean cannot drop the role holding its own session. That
single CREATE ROLE is filtered out of the stream while its ALTER ROLE
is kept, because that is what carries the attributes and the password.
Found by running it: the first replay died on `role "postgres"
already exists`.
- --empty means more than for one database: the cluster's databases go
first, then DROP OWNED BY releases what a role still holds in the
control database, then the roles themselves. The order is pinned by a
phase column because \gexec would otherwise emit them interleaved, and
a role cannot be dropped while it still owns a database.
Without --empty the replay stops at the first object that already
exists. Recreating a cluster over a populated one is a decision, not a
default.
The e2e test drills the real thing: two databases and their owning role
are dropped outright and have to come back with their payload and their
ownership intact.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>