Symptom
Both pytest (app) and pytest (migrations) fail at alembic upgrade head on every fresh resolve since 2026-09-25:
File ".../sqlalchemy/dialects/postgresql/psycopg.py", line 497, in import_dbapi
import psycopg
ModuleNotFoundError: No module named 'psycopg'
First seen on #37 (run 36342478818). main last ran green on 2026-09-19, before the release below, and has not run since; the failure is independent of #37's change.
Cause
uv.lock is not committed, so CI resolves fresh. SQLAlchemy 2.1.1 was uploaded 2026-09-25 and pyproject.toml declares sqlalchemy[asyncio] with no upper bound, so CI now gets 2.1.
- SQLAlchemy 2.1 changed the default PostgreSQL driver:
postgresql:// now means psycopg (3), no longer psycopg2 (migration notes: "Default PostgreSQL driver changed to psycopg (psycopg 3)").
app/settings.py:60 builds the sync DSN for Alembic with .set(drivername="postgresql"), so migrations ask for the default driver, and the project depends on psycopg2, not psycopg.
Options
- Name the driver:
drivername="postgresql+psycopg2". Smallest change, pins the meaning of the URL regardless of SQLAlchemy's default.
- Move to psycopg 3: replace
psycopg2 with psycopg[binary] and keep postgresql://. Aligns with SQLAlchemy's direction; larger change.
- Cap
sqlalchemy<2.1 until either of the above. Buys time only.
Either way the bare sqlalchemy[asyncio] dependency is what let a release change behaviour under CI; a lower bound and, for an application, a committed lock would have made this a deliberate upgrade.
Symptom
Both
pytest (app)andpytest (migrations)fail atalembic upgrade headon every fresh resolve since 2026-09-25:First seen on #37 (run 36342478818).
mainlast ran green on 2026-09-19, before the release below, and has not run since; the failure is independent of #37's change.Cause
uv.lockis not committed, so CI resolves fresh. SQLAlchemy 2.1.1 was uploaded 2026-09-25 andpyproject.tomldeclaressqlalchemy[asyncio]with no upper bound, so CI now gets 2.1.postgresql://now means psycopg (3), no longer psycopg2 (migration notes: "Default PostgreSQL driver changed to psycopg (psycopg 3)").app/settings.py:60builds the sync DSN for Alembic with.set(drivername="postgresql"), so migrations ask for the default driver, and the project depends onpsycopg2, notpsycopg.Options
drivername="postgresql+psycopg2". Smallest change, pins the meaning of the URL regardless of SQLAlchemy's default.psycopg2withpsycopg[binary]and keeppostgresql://. Aligns with SQLAlchemy's direction; larger change.sqlalchemy<2.1until either of the above. Buys time only.Either way the bare
sqlalchemy[asyncio]dependency is what let a release change behaviour under CI; a lower bound and, for an application, a committed lock would have made this a deliberate upgrade.