feat: add SQLAlchemy models, Alembic migrations, and DB-backed health check #3
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "2-add-sqlalchemy-models-alembic-migrations"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implements the full data persistence layer for mcd-tracker-api: SQLAlchemy ORM models (Location, CouponUsage), Alembic migration infrastructure with initial schema, database module, migration-on-startup, and a DB-backed health endpoint. All patterns follow basketball-api conventions.
Changes
src/mcd_tracker_api/database.py-- Engine, SessionLocal, Base, get_db() generator (basketball-api pattern)src/mcd_tracker_api/models.py-- Location (keycloak_sub, name, address, city, state, lat/lng) and CouponUsage (location FK with CASCADE, code, used_at, redeemed, redeemed_at, expires_at) modelsalembic.ini+alembic/env.py+alembic/script.py.mako-- Alembic config using MCD_TRACKER_DATABASE_URL env varalembic/versions/001_initial_schema.py-- Initial migration creating both tables with indexes on keycloak_sub and location_idsrc/mcd_tracker_api/main.py-- Lifespan now runs Alembic upgrade to head on startup (with graceful error handling)src/mcd_tracker_api/routes/health.py-- Health endpoint now executesSELECT 1and returns{"status": "ok", "db": "connected"}or{"status": "degraded", "db": "disconnected"}pyproject.toml-- Added sqlalchemy, psycopg2-binary, alembic dependenciesDockerfile-- Copies alembic.ini and alembic/ directory into runtime imagetests/conftest.py-- Postgres-backed test fixtures (create_all/drop_all per test, DB session, TestClient with dependency override)tests/test_health.py-- Updated for DB-backed health response, degraded simulation, metricstests/test_models.py-- 6 model tests: create location, create coupon usage, redeem, cascade delete, relationship, expires_at application-setTest Plan
mcd_tracker_testdatabase)ruff checkandruff format --checkpass cleanexpires_atconfirmed as application-set (not DB-generated)postgresql.ENUMorsa.Enumused (no enum columns in this schema)Review Checklist
sa.Enumusage -- avoids non-idempotent CREATE TYPEexpires_atset in application logic, not as Postgres generated columnMCD_TRACKER_DATABASE_URLenv var used consistentlyRelated
Closes #2
Implements the data persistence layer for mcd-tracker-api: - Location and CouponUsage SQLAlchemy models with cascade delete - Alembic migration infrastructure with 001_initial_schema - Database module (engine, SessionLocal, Base, get_db) - Migration-on-startup via lifespan handler - Health endpoint now returns {"status": "ok", "db": "connected"} or {"status": "degraded", "db": "disconnected"} - 9 tests against real Postgres (models, cascade, relationships, health) Closes #2 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>PR #3 Review
DOMAIN REVIEW
Tech stack: Python 3.12 / FastAPI / SQLAlchemy 2.0 / Alembic / Postgres / Pydantic Settings
This PR adds the full persistence layer: ORM models (
Location,CouponUsage), Alembic migration infrastructure with an initial schema, adatabase.pymodule following the basketball-api pattern, migration-on-startup in the FastAPI lifespan, a DB-backed/healthzendpoint, Dockerfile updates, and Postgres-backed tests. 497 additions, 9 deletions across 13 files.SQLAlchemy patterns:
Mapped[T]+mapped_column()-- modern SQLAlchemy 2.0 style. Good.DeclarativeBaseused correctly.get_db()uses try/finally/close -- correct.cascade="all, delete-orphan"on the parent side +ondelete="CASCADE"on the FK -- both ORM-level and DB-level cascade. Solid.sa.Enumusage -- explicitly avoids the non-idempotentCREATE TYPEfootgun learned from basketball-api migration 008. Good.Alembic patterns:
env.pyreadsMCD_TRACKER_DATABASE_URLfrom environment, overrides thedriver://placeholder inalembic.ini. Correct.RuntimeErrorif missing). Good guard.FastAPI patterns:
Depends(get_db)for DI -- correct.text("SELECT 1")-- safe, no injection surface.Test infrastructure:
mcd_tracker_testDB), not mocks. Good.conftest.pyusescreate_all/drop_allper test (autouse fixture) -- clean isolation.raise_server_exceptions=Falseon TestClient to prevent Alembic lifespan conflicts with conftest table creation. Documented in docstring. Reasonable tradeoff.db.executeto simulate failure. Works but fragile -- acceptable for this scope.Dockerfile:
alembic.ini,alembic/) copied into runtime image. Correct.BLOCKERS
None. All blocker criteria pass:
NITS
.gitignoremissing.env-- The.gitignoredoes not include.envor*.env. Sincepydantic-settingsencourages.envfiles for local dev (withMCD_TRACKER_DATABASE_URL), there is a risk of accidentally committing one with credentials. Add.envand*.envto.gitignore.CI test step has no Postgres service -- The Woodpecker CI
teststep runspytestbut there is no database service container configured. Theconftest.pydefaults topostgresql://mcd_tracker:mcd_tracker@localhost:5432/mcd_tracker_test. This will fail in CI unless a Postgres sidecar is added. This is likely a known deferral (scaffolding first, CI DB service later), but worth flagging.Migration failure is swallowed on startup --
_run_migrations()catches all exceptions and only logs them. The app will start with an un-migrated database, which could lead to confusing runtime errors. Consider whether a failed migration should prevent startup (raise instead of swallow). The basketball-api does the same thing, so this is consistent, but it is a design choice worth revisiting as the schema grows.alembic/env.pycreates its own engine -- Therun_migrations_online()function creates a newcreate_engine(db_url)from the raw env var, whiledatabase.pyalso creates an engine fromsettings.database_url(which reads the same env var via pydantic-settings). This is two engine instances for the same URL. Not a bug (Alembic CLI needs its own engine), but worth a comment noting the intentional duplication.conftest.pyhardcodes default credentials -- Line 8:"postgresql://mcd_tracker:mcd_tracker@localhost:5432/mcd_tracker_test". These are test-only local defaults and not production credentials, so this is not a blocker. But the passwordmcd_trackerfor the test database is embedded in source. Fine for local dev, just noting it.alembic.inihassqlalchemy.url = driver://-- This placeholder is overridden at runtime by bothenv.pyandmain.py. It works, but someone running barealembic upgrade headwithout the env var set will get a confusingdriver://connection error rather than the clearRuntimeErrorfromenv.py(because the offline path falls back to this value). Minor -- the online path has the guard.No
updated_atcolumn -- NeitherLocationnorCouponUsagehas anupdated_attimestamp. Not needed now, but will likely be needed when CRUD routes are added (for tracking when coupons are redeemed, locations edited, etc.). Consider adding it in the initial schema to avoid a migration later.SOP COMPLIANCE
2-add-sqlalchemy-models-alembic-migrationsreferences issue #2)## Summary,## Changes,## Test Plan,## RelatedForgejo issue: #2but does not referenceplan-mcd-tracker. Minor gap.Closes #2present in PR body.envfiles, or credentials committedPROCESS OBSERVATIONS
mcd_trackerandmcd_tracker_testdatabases, required env vars) will help onboarding.VERDICT: APPROVED
Clean persistence layer. No blockers. The nits (
.gitignore.envexclusion, CI Postgres service, plan slug reference) should be addressed in subsequent work but do not block merge.