mantimon-tcg/backend/app/db/migrations/env.py
Cal Corum 50684a1b11 Add database infrastructure with SQLAlchemy models and test suite
Phase 1 Database Implementation (DB-001 through DB-012):

Models:
- User: OAuth support (Google/Discord), premium subscriptions
- Collection: Card ownership with CardSource enum
- Deck: JSONB cards/energy_cards, validation state
- CampaignProgress: One-to-one with User, medals/NPCs as JSONB
- ActiveGame: In-progress games with GameType enum
- GameHistory: Completed games with EndReason enum, replay data

Infrastructure:
- Alembic migrations with sync psycopg2 (avoids async issues)
- Docker Compose for Postgres (5433) and Redis (6380)
- App config with Pydantic settings
- Redis client helper

Test Infrastructure:
- 68 database tests (47 model + 21 relationship)
- Async factory pattern for test data creation
- Sync TRUNCATE cleanup (solves pytest-asyncio event loop mismatch)
- Uses dev containers instead of testcontainers for reliability

Key technical decisions:
- passive_deletes=True for ON DELETE SET NULL relationships
- NullPool for test sessions (no connection reuse)
- expire_on_commit=False with manual expire() for relationship tests
2026-01-27 10:17:30 -06:00

109 lines
2.7 KiB
Python

"""Alembic migration environment configuration.
This module configures Alembic to work with SQLAlchemy and
automatically detect model changes for migration generation.
Supports both sync (psycopg2) and async (asyncpg) database URLs.
"""
from logging.config import fileConfig
from alembic import context
from sqlalchemy import create_engine, pool
from sqlalchemy.engine import Connection
# Import our models to ensure they're registered with Base.metadata
from app.db.base import Base
from app.db.models import ( # noqa: F401
ActiveGame,
CampaignProgress,
Collection,
Deck,
GameHistory,
User,
)
# Alembic Config object
config = context.config
# Interpret the config file for Python logging
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# Model metadata for 'autogenerate' support
target_metadata = Base.metadata
def get_url() -> str:
"""Get the database URL from alembic config.
This allows the URL to be set via command line or programmatically.
Converts asyncpg URLs to psycopg2 for sync operation.
"""
url = config.get_main_option("sqlalchemy.url")
if url and "asyncpg" in url:
url = url.replace("postgresql+asyncpg://", "postgresql+psycopg2://")
return url
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL and not an Engine,
though an Engine is acceptable here as well. By skipping the
Engine creation we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = get_url()
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
compare_type=True,
)
with context.begin_transaction():
context.run_migrations()
def do_run_migrations(connection: Connection) -> None:
"""Run migrations with the given connection."""
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True,
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine and associate
a connection with the context.
Uses synchronous psycopg2 driver for reliability.
"""
url = get_url()
connectable = create_engine(
url,
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
do_run_migrations(connection)
connectable.dispose()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()