Updated CLAUDE.md files with: - Current test counts and status - Session injection pattern documentation - New module references and architecture notes - Updated Phase status (3E-Final complete) - Enhanced troubleshooting guides Files updated: - Root CLAUDE.md: Project overview and phase status - backend/CLAUDE.md: Backend overview with test counts - backend/README.md: Quick start and development guide - backend/app/api/CLAUDE.md: API routes documentation - backend/app/database/CLAUDE.md: Session injection docs - backend/app/utils/CLAUDE.md: Utilities documentation - backend/tests/CLAUDE.md: Testing patterns and policy - frontend-sba/CLAUDE.md: Frontend overview - frontend-sba/store/CLAUDE.md: Store patterns 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
187 lines
5.7 KiB
Markdown
187 lines
5.7 KiB
Markdown
# Database Module - Async PostgreSQL Persistence
|
|
|
|
## Purpose
|
|
|
|
Async PostgreSQL persistence layer using SQLAlchemy 2.0. Handles all database operations with connection pooling and proper transaction management.
|
|
|
|
**Schema Management**: Alembic migrations (see `backend/README.md` for commands)
|
|
|
|
## Structure
|
|
|
|
```
|
|
app/database/
|
|
├── __init__.py # Package exports
|
|
├── session.py # Async session management, Base declarative
|
|
└── operations.py # DatabaseOperations class (session injection pattern)
|
|
|
|
backend/alembic/ # Migration scripts (managed by Alembic)
|
|
├── env.py # Migration environment config
|
|
└── versions/ # Migration files (001, 004, 005, etc.)
|
|
```
|
|
|
|
## Session Injection Pattern (IMPORTANT)
|
|
|
|
`DatabaseOperations` supports **session injection** to enable:
|
|
- **Transaction grouping**: Multiple operations in one atomic transaction
|
|
- **Test isolation**: Tests inject sessions with automatic rollback
|
|
- **Connection efficiency**: Avoids asyncpg "operation in progress" conflicts
|
|
|
|
### Two Usage Modes
|
|
|
|
#### Mode 1: Standalone (Default) - Each operation auto-commits
|
|
```python
|
|
from app.database.operations import DatabaseOperations
|
|
|
|
db_ops = DatabaseOperations() # No session injected
|
|
await db_ops.create_game(...) # Creates session, commits, closes
|
|
await db_ops.save_play(...) # Creates NEW session, commits, closes
|
|
```
|
|
|
|
#### Mode 2: Session Injection - Caller controls transaction
|
|
```python
|
|
from app.database.operations import DatabaseOperations
|
|
from app.database.session import AsyncSessionLocal
|
|
|
|
async with AsyncSessionLocal() as session:
|
|
try:
|
|
db_ops = DatabaseOperations(session) # Inject session
|
|
await db_ops.save_play(...) # Uses injected session (flush only)
|
|
await db_ops.update_game_state(...) # Same session (flush only)
|
|
await session.commit() # Caller commits once
|
|
except Exception:
|
|
await session.rollback()
|
|
raise
|
|
```
|
|
|
|
### When to Use Each Mode
|
|
|
|
| Scenario | Mode | Why |
|
|
|----------|------|-----|
|
|
| Single operation | Standalone | Simpler, auto-commits |
|
|
| Multiple related operations | Session injection | Atomic transaction |
|
|
| Integration tests | Session injection | Rollback after test |
|
|
| game_engine.py transactions | Session injection | save_play + update_game_state atomic |
|
|
|
|
### Internal Implementation
|
|
|
|
```python
|
|
class DatabaseOperations:
|
|
def __init__(self, session: AsyncSession | None = None):
|
|
self._session = session
|
|
|
|
@asynccontextmanager
|
|
async def _get_session(self):
|
|
if self._session:
|
|
yield self._session # Caller controls commit/rollback
|
|
else:
|
|
async with AsyncSessionLocal() as session:
|
|
try:
|
|
yield session
|
|
await session.commit()
|
|
except Exception:
|
|
await session.rollback()
|
|
raise
|
|
```
|
|
|
|
**Key Detail**: Methods use `flush()` not `commit()` - this persists changes within the transaction but doesn't finalize it. For injected sessions, the caller commits. For standalone, the context manager commits.
|
|
|
|
## DatabaseOperations API
|
|
|
|
### Game Operations
|
|
```python
|
|
from app.database.operations import DatabaseOperations
|
|
db_ops = DatabaseOperations()
|
|
|
|
await db_ops.create_game(game_id, league_id, home_team_id, away_team_id, ...)
|
|
await db_ops.update_game_state(game_id, inning, half, home_score, away_score)
|
|
game_data = await db_ops.load_game_state(game_id)
|
|
```
|
|
|
|
### Play Operations
|
|
```python
|
|
play_id = await db_ops.save_play(play_data)
|
|
plays = await db_ops.get_plays(game_id, limit=100)
|
|
```
|
|
|
|
### Lineup Operations
|
|
```python
|
|
lineup_id = await db_ops.add_pd_lineup_card(game_id, team_id, card_id, position, batting_order)
|
|
lineup_id = await db_ops.add_sba_lineup_player(game_id, team_id, player_id, position, batting_order)
|
|
lineup = await db_ops.get_active_lineup(game_id, team_id)
|
|
await db_ops.deactivate_lineup_player(lineup_id)
|
|
```
|
|
|
|
### Roster Operations
|
|
```python
|
|
await db_ops.add_pd_roster_card(game_id, card_id, team_id)
|
|
await db_ops.add_sba_roster_player(game_id, player_id, team_id)
|
|
roster = await db_ops.get_pd_roster(game_id, team_id)
|
|
```
|
|
|
|
### Session Operations
|
|
```python
|
|
await db_ops.create_game_session(game_id)
|
|
await db_ops.update_session_snapshot(game_id, state_snapshot)
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
### Transaction Grouping (Recommended for related operations)
|
|
```python
|
|
async with AsyncSessionLocal() as session:
|
|
try:
|
|
db_ops = DatabaseOperations(session)
|
|
await db_ops.save_play(play_data)
|
|
await db_ops.update_game_state(game_id, ...)
|
|
await session.commit() # Both succeed or both fail
|
|
except Exception:
|
|
await session.rollback()
|
|
raise
|
|
```
|
|
|
|
### Efficient Queries
|
|
```python
|
|
# Use joinedload for relationships needed immediately
|
|
from sqlalchemy.orm import joinedload
|
|
|
|
query = select(Play).options(
|
|
joinedload(Play.batter),
|
|
joinedload(Play.pitcher)
|
|
)
|
|
```
|
|
|
|
### Direct UPDATE
|
|
```python
|
|
# More efficient than SELECT + modify + commit
|
|
stmt = update(Game).where(Game.id == game_id).values(inning=5)
|
|
await session.execute(stmt)
|
|
```
|
|
|
|
## Database Schema
|
|
|
|
See `../models/CLAUDE.md` for model details.
|
|
|
|
| Table | Purpose |
|
|
|-------|---------|
|
|
| games | Game container, scores, status |
|
|
| plays | At-bat records with 25+ stats |
|
|
| lineups | Player assignments, substitutions |
|
|
| game_sessions | WebSocket state |
|
|
| roster_links | Eligible cards/players |
|
|
|
|
## Environment
|
|
|
|
```bash
|
|
DATABASE_URL=postgresql+asyncpg://user:pass@10.10.0.42:5432/paperdynasty_dev
|
|
```
|
|
|
|
## References
|
|
|
|
- **Migrations**: See `backend/README.md` for Alembic commands
|
|
- **Models**: See `../models/CLAUDE.md`
|
|
- **State Recovery**: See `../core/state_manager.py`
|
|
|
|
---
|
|
|
|
**Tests**: `tests/integration/database/` (32 tests) | **Updated**: 2025-11-27
|