From c09082772ba252ce2c01b7f6a5a08a73587ae9f1 Mon Sep 17 00:00:00 2001 From: Max Petrusenko Date: Wed, 2 Sep 2026 00:55:10 -0400 Subject: [PATCH] fix(autogenstudio): preserve DB password in alembic.ini (#7341) SQLAlchemy 2.x masks passwords in str(URL) as ***, so the URL written to alembic.ini by SchemaManager._generate_alembic_ini_content() ended with a literal *** password. On first-time startup against PostgreSQL, create_all() succeeded with the real engine but Alembic generate_revision authenticated with *** and failed with 'password authentication failed', even though the same credentials worked via psql. Use URL.render_as_string(hide_password=False) so the real password is written, keeping the %% ConfigParser escaping for passwords that contain %. Adds regression tests for the plain and special-character password cases. --- .../autogenstudio/database/schema_manager.py | 6 ++- .../tests/test_schema_manager.py | 40 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 python/packages/autogen-studio/tests/test_schema_manager.py diff --git a/python/packages/autogen-studio/autogenstudio/database/schema_manager.py b/python/packages/autogen-studio/autogenstudio/database/schema_manager.py index 0762b0890d30..e80948fb2104 100644 --- a/python/packages/autogen-studio/autogenstudio/database/schema_manager.py +++ b/python/packages/autogen-studio/autogenstudio/database/schema_manager.py @@ -194,7 +194,11 @@ def _generate_alembic_ini_content(self) -> str: """ Generates content for alembic.ini file. """ - engine_url = str(self.engine.url).replace("%", "%%") + # render_as_string(hide_password=False) is required because str(URL) masks the + # password as "***" in SQLAlchemy 2.x, which would make Alembic authenticate + # with the literal "***" and fail (see issue #7341). "%" must still be escaped + # for ConfigParser interpolation. + engine_url = self.engine.url.render_as_string(hide_password=False).replace("%", "%%") return f""" [alembic] script_location = {self.alembic_dir} diff --git a/python/packages/autogen-studio/tests/test_schema_manager.py b/python/packages/autogen-studio/tests/test_schema_manager.py new file mode 100644 index 000000000000..ad632499a151 --- /dev/null +++ b/python/packages/autogen-studio/tests/test_schema_manager.py @@ -0,0 +1,40 @@ +import configparser +from pathlib import Path +from types import SimpleNamespace + +from sqlalchemy.engine import make_url + +from autogenstudio.database.schema_manager import SchemaManager + + +def _alembic_ini_url(engine_uri: str) -> str: + """Generate the alembic.ini content for a URI and return the parsed sqlalchemy.url.""" + # _generate_alembic_ini_content() only reads engine.url, so a lightweight stub + # keeps this pure URL-rendering test free of DBAPI driver imports (psycopg/libpq). + engine = SimpleNamespace(url=make_url(engine_uri)) + schema_manager = SchemaManager(engine=engine, base_dir=Path("/tmp/schema_manager_test")) # type: ignore[arg-type] + content = schema_manager._generate_alembic_ini_content() + parser = configparser.ConfigParser() + parser.read_string(content) + return parser.get("alembic", "sqlalchemy.url") + + +def test_alembic_ini_preserves_database_password(): + """Regression test for #7341: the URL written to alembic.ini must keep the real + password. SQLAlchemy 2.x masks passwords in ``str(URL)`` as ``***``, so writing + that form into alembic.ini made Alembic authenticate with the literal ``***`` and + fail with "password authentication failed" on first-time database initialization + even though the application engine itself connected successfully.""" + ini_url = _alembic_ini_url("postgresql+psycopg://autogen:autogen@postgres:5432/autogen") + assert make_url(ini_url).password == "autogen" + assert "***" not in ini_url + + +def test_alembic_ini_roundtrips_password_with_special_characters(): + """Passwords with URL-reserved characters must survive the alembic.ini round trip + (URL encoding on render + ConfigParser ``%%`` escaping).""" + ini_url = _alembic_ini_url("postgresql+psycopg://alice:p%40ssw0rd@db.example.com:5432/app") + parsed = make_url(ini_url) + assert parsed.username == "alice" + assert parsed.password == "p@ssw0rd" + assert parsed.host == "db.example.com"