Skip to content

Commit 3c09d06

Browse files
committed
fix(postgres): failed loudly on a plain-SQL restore error
A plain-format restore ran `psql --quiet` without ON_ERROR_STOP, so psql swallowed per-statement errors and still exited 0. Restoring a plain dump onto a non-empty database (e.g. a live instance whose schema was already created on boot) therefore reported success while restoring little or nothing. Added `--set ON_ERROR_STOP=1` to both plain psql branches so the first failed statement aborts with a non-zero exit, which the runner surfaces as a failed restore. No change on the intended fresh/empty-DB path. Surfaced by the NocoDB two-mode restore audit (DB-mode strategy).
1 parent 0c75abe commit 3c09d06

2 files changed

Lines changed: 16 additions & 2 deletions

File tree

‎src/backuphelper/sources/postgres.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,12 @@ def build_restore_argv(cfg: PostgresConfig, dump: Path) -> list[str]:
106106
if suffix.endswith(".dump"):
107107
return ["pg_restore", "--clean", "--if-exists", "--no-owner", "--no-acl",
108108
"--single-transaction", "--dbname", cfg.database, str(dump)]
109+
# ON_ERROR_STOP=1 makes psql exit non-zero on the first failed statement
110+
# instead of swallowing errors and exiting 0 — so a plain-SQL restore onto a
111+
# non-empty DB fails loudly instead of reporting a false success.
109112
if suffix.endswith(".sql.gz"):
110-
return ["psql", "--quiet"] # dump is streamed to stdin (gunzipped)
111-
return ["psql", "--quiet", "--file", str(dump)]
113+
return ["psql", "--quiet", "--set", "ON_ERROR_STOP=1"] # dump streamed to stdin (gunzipped)
114+
return ["psql", "--quiet", "--set", "ON_ERROR_STOP=1", "--file", str(dump)]
112115

113116

114117
def _pg_restore(cfg: PostgresConfig, dump: Path, run: RunFn) -> None:

‎tests/sources/test_postgres.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,17 @@ def test_restore_argv_for_plain_sql():
102102
assert argv[0] == "psql"
103103

104104

105+
def test_plain_restore_stops_on_first_error():
106+
# A plain psql restore must fail loudly (ON_ERROR_STOP=1) instead of swallowing
107+
# per-statement errors and exiting 0 — otherwise a restore onto a non-empty DB
108+
# reports success while restoring nothing.
109+
from backuphelper.sources.postgres import build_restore_argv
110+
for dump in (Path("/r/database.sql.gz"), Path("/r/database.sql")):
111+
argv = build_restore_argv(PostgresSource(_cfg()).cfg, dump)
112+
assert argv[0] == "psql"
113+
assert "--set" in argv and "ON_ERROR_STOP=1" in argv, argv
114+
115+
105116
def test_restore_runs_pg_restore_for_dump(tmp_path):
106117
# component name defaults to the database name ("logto")
107118
(tmp_path / "logto.dump").write_bytes(b"x")

0 commit comments

Comments
 (0)