Skip to content

fix: normalize async Postgres URLs for alembic - #420

Open
lkshrk wants to merge 1 commit into
OpenHands:mainfrom
lkshrk:fix/alembic-async-url-normalization
Open

lkshrk wants to merge 1 commit into
OpenHands:mainfrom
lkshrk:fix/alembic-async-url-normalization

Conversation

@lkshrk

@lkshrk lkshrk commented Sep 4, 2026

Copy link
Copy Markdown

HUMAN:

Reproduced against a self-hosted deployment on Kubernetes with a CloudNativePG Postgres and AUTOMATION_DB_URL=postgresql+asyncpg://…: alembic upgrade head fails with MissingGreenlet, and hand-rewriting the URL to postgresql+pg8000:// makes it pass. Ran uv run pytest tests/test_db.py and pre-commit locally on this branch.

AGENT:

Problem

AUTOMATION_DB_URL is the documented way to point a self-hosted deployment at a
database, and the Postgres form of it is postgresql+asyncpg://… — that is what
the agent-canvas Helm chart sets, and what db.py expects for the application
engine.

Alembic cannot use it. migrations/env.py get_engine() reads
AUTOMATION_DB_URL from the environment and passes it straight to the sync
sqlalchemy.create_engine(), normalizing only the SQLite prefix:

if DB_URL:
    url = DB_URL
    if url.startswith("sqlite+aiosqlite"):
        url = url.replace("sqlite+aiosqlite", "sqlite", 1)
    return create_engine(url, pool_pre_ping=True)

With an asyncpg URL, alembic upgrade head fails:

sqlalchemy.exc.MissingGreenlet: greenlet_spawn has not been called;
can't call await_only() here.

Rewriting the URL by hand to postgresql+pg8000://… makes the same command
succeed, which is the shape of the fix: pg8000 is already a dependency and is
already the driver env.py uses for its host/port and Cloud SQL paths.

Fix

  • db.py: normalize_sqlite_url_for_alembic() becomes
    normalize_url_for_alembic(), driven by a small ALEMBIC_SYNC_DRIVERS map —
    sqlite+aiosqlitesqlite (unchanged behaviour) and
    postgresql+asyncpgpostgresql+pg8000. Only a leading driver prefix is
    rewritten, so a URL whose password happens to contain the driver name is left
    alone. Every other URL passes through untouched.
  • migrations/env.py: get_engine() and run_migrations_offline() both run a
    configured AUTOMATION_DB_URL through the normalizer. A URL that resolves to
    pg8000 now also gets _build_pg8000_connect_args(DB_SSL_MODE), so
    AUTOMATION_DB_SSL_MODE finally applies to the URL path the same way it
    already applies to the host/port path.
  • app.py: uses the renamed helper when it sets sqlalchemy.url.

The helper is renamed rather than aliased, because an alias could not stay
backward compatible: the old name's contract was "asyncpg URLs are unchanged",
which is precisely the bug. Both call sites and the tests are updated; nothing
outside this repo imports it.

Verification

  • uv run pytest tests/test_db.py — 30 passed. The normalizer suite gained
    cases for asyncpg → pg8000, prefix-only rewriting, a non-async Postgres
    driver, and the empty string.
  • Full unit suite and pre-commit (ruff format, ruff lint, pycodestyle,
    pyright) clean on the changed files.
  • AUTOMATION_DB_URL=postgresql+asyncpg://… alembic upgrade head --sql renders
    Postgres DDL, and the previously failing online path now builds a pg8000
    engine instead of an asyncpg one.

migrations/env.py executes run_migrations_online() at import time, so it has
no direct unit test; the behaviour is covered through the shared helper.

No schema change, no new dependency, no behaviour change for SQLite or for
deployments configured via AUTOMATION_DB_HOST/AUTOMATION_DB_PORT.

@all-hands-bot

Copy link
Copy Markdown
Contributor

👋 This PR needs a couple of things fixed before OpenHands can review it:

  • the PR description's HUMAN: section needs at least 20 characters describing what you tested, not just the template placeholder

Push an update once this is addressed and this check re-runs automatically.

This is an automated check - no AI was used to generate this comment.

`AUTOMATION_DB_URL=postgresql+asyncpg://...` is the documented way to point a
self-hosted deployment at Postgres, but `migrations/env.py` handed that URL
straight to the sync `create_engine()`, so `alembic upgrade head` failed with
`sqlalchemy.exc.MissingGreenlet`. Only the `sqlite+aiosqlite` prefix was
rewritten to a sync driver.

Generalize the helper into `normalize_url_for_alembic()`, which also maps
`postgresql+asyncpg` to `postgresql+pg8000` — already a dependency, and already
the driver `env.py` uses for its host/port path — and apply it everywhere a
configured URL reaches alembic. A URL-configured Postgres engine now gets the
same `AUTOMATION_DB_SSL_MODE` connect args as the host/port path.

Claude-Session: https://claude.ai/code/session_01T5LhMv1aKbckQ6SLd4sPGZ
@lkshrk
lkshrk force-pushed the fix/alembic-async-url-normalization branch from 082ad5f to 30730df Compare September 13, 2026 05:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: fix A bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants