mantimon-tcg/backend/app/db/base.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

105 lines
2.9 KiB
Python

"""SQLAlchemy base model and common mixins.
This module provides the declarative base class and common column mixins
used by all database models.
Example:
from app.db.base import Base
class User(Base):
__tablename__ = "users"
email = Column(String, unique=True, nullable=False)
display_name = Column(String, nullable=False)
"""
from datetime import datetime
from typing import Any
from uuid import uuid4
from sqlalchemy import DateTime, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import DeclarativeBase, Mapped, declared_attr, mapped_column
class Base(DeclarativeBase):
"""Base class for all SQLAlchemy models.
Provides common columns and functionality:
- id: UUID primary key (auto-generated)
- created_at: Timestamp when record was created
- updated_at: Timestamp when record was last updated
All models should inherit from this class.
Example:
class User(Base):
__tablename__ = "users"
email: Mapped[str] = mapped_column(unique=True)
"""
# Type annotation for type checkers
__tablename__: str
# Common columns for all models
id: Mapped[str] = mapped_column(
UUID(as_uuid=False),
primary_key=True,
default=lambda: str(uuid4()),
doc="Unique identifier (UUID)",
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
doc="Timestamp when record was created",
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
doc="Timestamp when record was last updated",
)
@declared_attr.directive
@classmethod
def __tablename__(cls) -> str:
"""Generate table name from class name.
Converts CamelCase to snake_case and pluralizes.
Example: UserProfile -> user_profiles
Can be overridden by setting __tablename__ directly.
"""
# Convert CamelCase to snake_case
name = cls.__name__
result = [name[0].lower()]
for char in name[1:]:
if char.isupper():
result.append("_")
result.append(char.lower())
else:
result.append(char)
# Simple pluralization (add 's')
table_name = "".join(result)
if not table_name.endswith("s"):
table_name += "s"
return table_name
def to_dict(self) -> dict[str, Any]:
"""Convert model instance to dictionary.
Returns:
Dictionary with all column values.
"""
return {column.name: getattr(self, column.name) for column in self.__table__.columns}
def __repr__(self) -> str:
"""Generate string representation of model instance."""
class_name = self.__class__.__name__
return f"<{class_name}(id={self.id!r})>"