From df7f6eb6934176e0330dff7430d23d99156c70c3 Mon Sep 17 00:00:00 2001 From: Yvonne Yao Date: Mon, 3 Aug 2026 13:00:18 +1000 Subject: [PATCH] fix: escape database URL for Alembic config --- backend/alembic/env.py | 6 +++++- backend/tests/test_alembic_env.py | 33 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 backend/tests/test_alembic_env.py diff --git a/backend/alembic/env.py b/backend/alembic/env.py index a0a3418..ee51a73 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -18,6 +18,10 @@ def database_url() -> str: return getenv("DATABASE_URL") or config.get_main_option("sqlalchemy.url") +def escape_database_url_for_alembic(url: str) -> str: + return url.replace("%", "%%") + + def run_migrations_offline() -> None: context.configure( url=database_url(), @@ -31,7 +35,7 @@ def run_migrations_offline() -> None: def run_migrations_online() -> None: - config.set_main_option("sqlalchemy.url", database_url()) + config.set_main_option("sqlalchemy.url", escape_database_url_for_alembic(database_url())) connectable = engine_from_config( config.get_section(config.config_ini_section, {}), prefix="sqlalchemy.", diff --git a/backend/tests/test_alembic_env.py b/backend/tests/test_alembic_env.py new file mode 100644 index 0000000..bec6847 --- /dev/null +++ b/backend/tests/test_alembic_env.py @@ -0,0 +1,33 @@ +import ast +import configparser +from pathlib import Path +from types import FunctionType + + +def load_env_helper(name: str) -> FunctionType: + env_path = Path(__file__).resolve().parents[1] / "alembic" / "env.py" + module = ast.parse(env_path.read_text()) + helper = next( + node for node in module.body if isinstance(node, ast.FunctionDef) and node.name == name + ) + compiled = compile( + ast.Module(body=[helper], type_ignores=[]), filename=str(env_path), mode="exec" + ) + namespace: dict[str, object] = {} + exec(compiled, namespace) + return namespace[name] # type: ignore[return-value] + + +def test_alembic_database_url_escapes_percent_signs_for_configparser() -> None: + escape_url = load_env_helper("escape_database_url_for_alembic") + database_url = ( + "postgresql+psycopg://happy_post_admin:example%2Bpassword%25" + "@localhost:5432/happy_post" + ) + + escaped_url = escape_url(database_url) + parser = configparser.ConfigParser() + parser.add_section("alembic") + parser.set("alembic", "sqlalchemy.url", escaped_url) + + assert escaped_url == database_url.replace("%", "%%")