fix(config): normalize the postgres:// short scheme for the async ORM engine (#4293)

`app_sqlalchemy_url` rewrites `postgresql://` to `postgresql+asyncpg://` but
leaves libpq's `postgres://` short scheme untouched, so it reaches
`create_async_engine` verbatim and raises
`NoSuchModuleError: Can't load plugin: sqlalchemy.dialects:postgres`.

The two consumers of the same `database.postgres_url` disagree about that
scheme, which is what makes this a partial, backend-only failure rather than a
clean config error:

  - the checkpointer and store pass the raw URL to psycopg
    (`runtime/checkpointer/provider.py`, `runtime/store/provider.py`), and
    psycopg's `conninfo_to_dict` accepts `postgres://` and `postgresql://`
    identically;
  - the application ORM engine goes through `app_sqlalchemy_url`
    (`persistence/engine.py:181`), and SQLAlchemy dropped the `postgres`
    dialect alias in 2.0.

So a `postgres://` DSN brings the checkpointer up and takes the ORM engine
down. The `postgres_url` field docstring already promises the opposite --
"the +asyncpg driver suffix is added automatically where needed".

`postgres://` is a legal libpq URI scheme, not a typo, and is the form
`$DATABASE_URL` commonly takes on managed Postgres providers -- which is
exactly what the module docstring recommends configuring
(`postgres_url: $DATABASE_URL`).

Normalize it alongside `postgresql://`. The existing tests covered
`postgresql://` and `postgresql+asyncpg://` but not the short scheme; the new
case fails on main with the `NoSuchModuleError` above.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andrew Chen
2026-07-19 17:01:28 +08:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 90d511f3d2
commit dd2f73c1a1
2 changed files with 12 additions and 0 deletions
@@ -98,5 +98,8 @@ class DatabaseConfig(BaseModel):
url = self.postgres_url
if url.startswith("postgresql://"):
url = url.replace("postgresql://", "postgresql+asyncpg://", 1)
elif url.startswith("postgres://"):
# libpq's short alias: accepted by the psycopg checkpointer, but not a SQLAlchemy dialect.
url = url.replace("postgres://", "postgresql+asyncpg://", 1)
return url
raise ValueError(f"No SQLAlchemy URL for backend={self.backend!r}")
@@ -49,6 +49,15 @@ class TestDatabaseConfig:
assert url.startswith("postgresql+asyncpg://")
assert "u:p@h:5432/db" in url
def test_app_sqlalchemy_url_postgres_short_scheme(self):
c = DatabaseConfig(
backend="postgres",
postgres_url="postgres://u:p@h:5432/db",
)
url = c.app_sqlalchemy_url
assert url.startswith("postgresql+asyncpg://")
assert "u:p@h:5432/db" in url
def test_app_sqlalchemy_url_postgres_already_asyncpg(self):
c = DatabaseConfig(
backend="postgres",