fix: convert DATABASE_URL to asyncpg scheme for async SQLAlchemy engine

The K8s DATABASE_URL env var uses postgresql:// which makes
create_async_engine try to load psycopg2 (sync driver). Add
_ensure_async_dsn() to convert postgresql:// and postgres:// to
postgresql+asyncpg:// at engine creation time. asyncpg is already
in dependencies.
This commit is contained in:
2026-03-03 08:13:33 -08:00
parent 05dfdb3b8e
commit 1ce1924c41
3 changed files with 54 additions and 1 deletions
+1
View File
@@ -62,6 +62,7 @@ test-*.mjs
test-*.js
test_*.mjs
test_*.py
!api/tests/test_*.py
*-test-results.json
*-results.json
*-ui-inspection.json
+18 -1
View File
@@ -8,8 +8,25 @@ from bootnode.config import get_settings
settings = get_settings()
def _ensure_async_dsn(dsn: str) -> str:
"""Ensure the database DSN uses the asyncpg driver.
Converts ``postgresql://`` or ``postgres://`` to
``postgresql+asyncpg://`` so that SQLAlchemy's async engine
uses the correct driver regardless of how DATABASE_URL is set.
"""
if dsn.startswith("postgresql+asyncpg://"):
return dsn
if dsn.startswith("postgresql://"):
return dsn.replace("postgresql://", "postgresql+asyncpg://", 1)
if dsn.startswith("postgres://"):
return dsn.replace("postgres://", "postgresql+asyncpg://", 1)
return dsn
engine = create_async_engine(
str(settings.database_url),
_ensure_async_dsn(str(settings.database_url)),
pool_size=settings.db_pool_size,
max_overflow=settings.db_max_overflow,
echo=settings.debug,
+35
View File
@@ -0,0 +1,35 @@
"""Tests for database session DSN handling."""
from bootnode.db.session import _ensure_async_dsn
def test_postgresql_converted():
"""Standard postgresql:// is converted to asyncpg."""
dsn = "postgresql://user:pass@host:5432/db"
assert _ensure_async_dsn(dsn) == "postgresql+asyncpg://user:pass@host:5432/db"
def test_postgres_converted():
"""Short-form postgres:// is converted to asyncpg."""
dsn = "postgres://user:pass@host:5432/db"
assert _ensure_async_dsn(dsn) == "postgresql+asyncpg://user:pass@host:5432/db"
def test_asyncpg_unchanged():
"""Already correct DSN is returned as-is."""
dsn = "postgresql+asyncpg://user:pass@host:5432/db"
assert _ensure_async_dsn(dsn) == dsn
def test_production_dsn():
"""Matches the actual K8s DATABASE_URL format."""
dsn = "postgresql://bootnode:[email protected]:5432/bootnode"
expected = "postgresql+asyncpg://bootnode:[email protected]:5432/bootnode"
assert _ensure_async_dsn(dsn) == expected
def test_only_first_occurrence_replaced():
"""Only the scheme prefix is replaced, not embedded substrings."""
dsn = "postgresql://user:postgresql@host:5432/postgresql"
result = _ensure_async_dsn(dsn)
assert result == "postgresql+asyncpg://user:postgresql@host:5432/postgresql"