From dd2f73c1a18bb1b2849d5daa9fe53570c26ea308 Mon Sep 17 00:00:00 2001 From: Andrew Chen <48723787+chuenchen309@users.noreply.github.com> Date: Sun, 19 Jul 2026 17:01:28 +0800 Subject: [PATCH] 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) --- .../packages/harness/deerflow/config/database_config.py | 3 +++ backend/tests/test_persistence_scaffold.py | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/backend/packages/harness/deerflow/config/database_config.py b/backend/packages/harness/deerflow/config/database_config.py index 37cfd579d..bcd869069 100644 --- a/backend/packages/harness/deerflow/config/database_config.py +++ b/backend/packages/harness/deerflow/config/database_config.py @@ -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}") diff --git a/backend/tests/test_persistence_scaffold.py b/backend/tests/test_persistence_scaffold.py index baba88c4b..ba460b9af 100644 --- a/backend/tests/test_persistence_scaffold.py +++ b/backend/tests/test_persistence_scaffold.py @@ -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",