From ec44c653fec9d4bdb11131d56acaf1193194818b Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 22 May 2026 22:54:19 -0400 Subject: [PATCH] fc5: strip SQLAlchemy +psycopg/+asyncpg driver suffix when passing URL to pg_dump/psql Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/app/services/migrators/backup.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/backend/app/services/migrators/backup.py b/backend/app/services/migrators/backup.py index 88040fd..4d3cb96 100644 --- a/backend/app/services/migrators/backup.py +++ b/backend/app/services/migrators/backup.py @@ -16,6 +16,19 @@ from typing import Any _BACKUPS_DIRNAME = "_backups" +def _libpq_url(sa_url: str) -> str: + """Strip SQLAlchemy driver suffix so pg_dump/psql accept the URL. + + SQLAlchemy uses URLs like `postgresql+psycopg://...` or + `postgresql+asyncpg://...`. libpq tools (pg_dump, psql) only know + the plain `postgresql://` scheme. + """ + for driver in ("postgresql+psycopg", "postgresql+asyncpg", "postgresql+psycopg2"): + if sa_url.startswith(driver + "://"): + return "postgresql://" + sa_url[len(driver) + 3:] + return sa_url + + def _backups_dir(images_root: Path | None = None) -> Path: # Overridable for tests via monkeypatch. root = images_root if images_root is not None else Path("/images") @@ -49,7 +62,7 @@ def create_backup( manifest_path = out_dir / f"fc_{ts}.json" _run_subprocess( - ["pg_dump", "--no-owner", "--no-acl", "-f", str(sql_path), db_url], + ["pg_dump", "--no-owner", "--no-acl", "-f", str(sql_path), _libpq_url(db_url)], _test_ts=ts, ) _run_subprocess( @@ -104,7 +117,7 @@ def restore_backup( tar_path = Path(manifest["tar_path"]) _run_subprocess( - ["psql", "-d", db_url, "-f", str(sql_path)], + ["psql", "-d", _libpq_url(db_url), "-f", str(sql_path)], ) # Wipe everything in images_root EXCEPT _backups/ (we'd delete the backup