Phase 3: Schema Layer - Pydantic v2 migration, deduplication, and missing schemas

- Migrated all schemas to Pydantic v2 syntax (model_config, ConfigDict)
- Fixed mutable default in ProtoMessageBase using Field(default_factory=datetime.now)
- Consolidated CardCollection and Wishlist schemas in user_card_collection.py
- Created game_schemas.py with GameCreate, GameResponse, GameJoinRequest, etc.
- Created mtg_card_schemas.py with MtgCardResponse, MtgCardSearchRequest, etc.
- Added CardImportBatchCreate, CardImportBatchResponse, UserCardImportCreate/Response schemas
- Fixed duplicate UserCardImportRecord class between card_import_batch.py and user_card_import_record.py
- Updated __init__.py with comprehensive schema exports
- Created verify_schemas.py for schema-model matching verification
This commit is contained in:
2026-08-16 05:22:17 +00:00
parent 5f324cf8a9
commit bea91db64d
20 changed files with 2733 additions and 342 deletions
+314 -235
View File
@@ -1,77 +1,80 @@
# Backend Test Plan
# Backend Test Plan (Updated)
## Overview
This test plan is designed for iterative execution using sub-agents, with each sub-agent handling a specific phase to avoid exceeding the 300,000 token context limit. Each phase focuses on a distinct subsystem and includes specific test cases and verification steps.
**Last Updated:** 2026-06-08
**Status:** Phase 1 Complete - Database & Migration Tests Verified
---
## Test Phases
### Phase 1: Database & Migration Tests
### Phase 1: Database & Migration Tests ✅ COMPLETE
**Scope:** Alembic migrations, database schema, model relationships
**Sub-agent Task:** Verify all migrations run correctly and database schema is consistent
**Status:** ✅ Verified - All migrations functionally correct
#### Test Cases:
1. **Migration 000 (Empty)**
- Verify migration exists and is empty
- Check downgrade/upgrade functions exist
#### Actual Migration Structure:
2. **Migration 001 (Initial User Schema)**
- Run migration upgrade
- Verify all 16 tables created:
**Migration 000 (`000_base_tables.py`)** - Creates 8 base tables:
- `mtgonline_users` (base table)
- `mtgonline_decklist_files` (base table)
- `mtgonline_decklist_folders` (FK to mtgonline_users.id, self-ref)
- `mtgonline_decklist_files` (FK to mtgonline_decklist_folders.id, mtgonline_users.id)
- `mtgonline_rooms` (base table)
- `user_sessions`
- `deck_versions`
- `game_replays`
- `replay_players`
- `game_outcomes`
- `user_statistics`
- `user_card_collection`
- `card_wishlist`
- `user_groups`
- `group_members`
- `group_chat_messages`
- `user_networks`
- `network_members`
- `user_preferences`
- `user_activity_log`
- Verify foreign key constraints
- Verify indexes created
- Verify unique constraints
- Run migration downgrade
- Verify all tables dropped
- `mtgonline_rooms_gametypes` (FK to mtgonline_rooms.id)
- `mtgonline_bans` (FK to mtgonline_users.id)
- `mtgonline_log` (FK to mtgonline_rooms.id, mtgonline_users.id)
- `mtgonline_audit` (FK to mtgonline_users.id)
3. **Migration 002 (User Deck Building Tables)**
- Run migration upgrade
- Verify tables created:
- `mtgonline_decklist_cards`
- `mtgonline_decklist_precedents`
- `mtgonline_decklist_suggestions`
- Verify foreign keys to `mtgonline_decklist_files`
- Run migration downgrade
- Verify tables dropped
**Migration 001 (`001_initial_user_schema.py`)** - Creates 15 user-related tables:
- `user_sessions` (FK to mtgonline_users.id)
- `deck_versions` (FK to mtgonline_decklist_files.id)
- `game_replays` (FK to mtgonline_rooms.id)
- `replay_players` (FK to game_replays.id, mtgonline_users.id, mtgonline_decklist_files.id)
- `game_outcomes` (FK to mtgonline_users.id, game_replays.game_uuid)
- `user_statistics` (PK: user_id, FK to mtgonline_users.id)
- `user_card_collection` (FK to mtgonline_users.id)
- `card_wishlist` (FK to mtgonline_users.id)
- `user_groups` (FK to mtgonline_users.id)
- `group_members` (FK to user_groups.id, mtgonline_users.id)
- `group_chat_messages` (FK to user_groups.id, mtgonline_users.id)
- `user_networks` (FK to mtgonline_users.id)
- `network_members` (FK to user_networks.id, mtgonline_users.id)
- `user_preferences` (PK: user_id, FK to mtgonline_users.id)
- `user_activity_log` (FK to mtgonline_users.id)
4. **Migration 003 (MTG Cards Table)**
- Run migration upgrade
- Verify `mtgonline_cards` table created
- Verify columns and indexes
- Run migration downgrade
- Verify table dropped
**Migration 002 (`002_user_deck_building_tables.py`)** - Creates:
- `user_decks` (FK to mtgonline_users.id, mtgonline_decklist_folders.id)
5. **Migration 004 (Card Import Table)**
- Run migration upgrade
- Verify `mtgonline_card_imports` table created
- Verify foreign key to `mtgonline_users`
- Run migration downgrade
- Verify table dropped
**Migration 003 (`003_mtgonline_cards_table.py`)** - Creates:
- `mtgonline_cards` (base table)
- `user_deck_cards` (FK to user_decks.id, mtgonline_cards.id)
- `deck_precedents` (FK to mtgonline_users.id)
- `deck_precedent_cards` (FK to deck_precedents.id, mtgonline_cards.id)
- `card_suggestions` (FK to user_decks.id, mtgonline_cards.id, self-ref)
6. **Schema Consistency Checks**
- Verify all foreign keys reference existing tables
- Verify no circular dependencies
- Verify all tables have proper indexes
- Verify unique constraints are valid
**Migration 004 (`004_card_import_table.py`)** - Creates:
- `user_card_imports` (FK to mtgonline_users.id)
**Verification:** All migrations run successfully in order, schema is consistent, no orphaned foreign keys.
**Migration 005 (`005_missing_tables.py`)** - Creates:
- `mtg_sets` (base table)
- `mtg_cards` (FK to mtg_sets.id)
- `mtg_cards_mirror` (base table)
- `card_import_batches` (FK to mtgonline_users.id)
- `user_card_imports_confirmed` (FK to mtgonline_users.id, card_import_batches.id)
- `deck_card_links` (FK to mtgonline_decklist_files.id, mtg_cards_mirror.id)
#### Verification Results:
- ✅ All 6 migrations properly linked (000 → 001 → 002 → 003 → 004 → 005)
- ✅ All foreign keys reference tables created in same or earlier migrations
- ✅ No circular dependencies
- ✅ All indexes created on FK columns
- ✅ All unique constraints valid
- ✅ Downgrade functions properly drop tables in reverse dependency order
- ✅ All model imports in `env.py` and `__init__.py` consistent
**Note:** Migration 000 is NOT empty (creates 8 base tables). This is intentional and correct.
---
@@ -79,40 +82,64 @@ This test plan is designed for iterative execution using sub-agents, with each s
**Scope:** SQLAlchemy models, relationships, validation
**Sub-agent Task:** Verify all model definitions are correct and consistent
#### Actual Model Structure:
**1. Core Models (`app/models/models.py`)**
- `User` (`mtgonline_users`) - username, email, password_hash, salt, country, real_name, avatar_bmp, privlevel, is_active, is_banned, ban_reason, ban_ends, vip_status, vip_expiry, creation_date, last_login
- `MtonlineCard` (`mtgonline_cards`) - source_id, name, mana_cost, type_line, oracle_text, power, toughness, rarity, layout, artist, flavor_text, numbers, identifiers, images, image, set_code, set_name, card_parts, keywords, legalities, synced_at, created_at
- `DecklistFolder` (`mtgonline_decklist_folders`) - owner_id, name, parent_id, creation_date
- `DecklistFile` (`mtgonline_decklist_files`) - folder_id, owner_id, name, content, format, status, creation_date
- `Room` (`mtgonline_rooms`) - name, description, is_password_protected, password_hash, creation_date
- `RoomGameType` (`mtgonline_rooms_gametypes`) - room_id, name, description
- `Ban` (`mtgonline_bans`) - user_id, server_id, reason, moderators, ip_address, expiration_time, active, creation_date
- `GameLog` (`mtgonline_log`) - room_id, player_id, message, timestamp
- `AuditLog` (`mtgonline_audit`) - admin_id, action_type, target_user_id, details, ip_address, timestamp
**2. MTG Models (`app/models/mtg_models.py`)**
- `MtgSet` (`mtg_sets`) - code, name, type, release_date, base_set_size, total_size, is_foil_only, is_non_foil_only, digital, icon_svg_url, parent_code, mtgo_code, image, updated_at
- `MtgCard` (`mtg_cards`) - set_id, name, mana_cost, type_line, oracle_text, power, toughness, rarity, layout, artist, flavor_text, numbers, identifiers, images, image, updated_at
**3. Mirror Models (`app/models/mirror_models.py`)**
- `MtgCardMirror` (`mtg_cards_mirror`) - source_id, name, mana_cost, type_line, oracle_text, power, toughness, rarity, layout, artist, flavor_text, numbers, identifiers, images, image, card_parts, keywords, legalities, set_code, set_name, synced_at, created_at
- `DeckCardLink` (`deck_card_links`) - deck_id, card_id, quantity, zone
**4. User Data Models (`app/models/user_data.py`)**
- `UserSession` (`user_sessions`) - user_id, session_token_hash, ip_address, user_agent, created_at, expires_at, is_active
- `DeckVersion` (`deck_versions`) - deck_id, version_number, content, status, comment, created_at
- `GameReplay` (`game_replays`) - game_uuid, room_id, game_type, format, duration_seconds, start_time, end_time, status, replay_data, created_at, updated_at
- `ReplayPlayer` (`replay_players`) - replay_id, user_id, position, deck_id, won, lost, concession, turn_one, created_at
- `GameOutcome` (`game_outcomes`) - user_id, game_uuid, outcome, opponent_id, format, rating_before, rating_after, rating_change, created_at
- `UserStatistics` (`user_statistics`) - user_id (PK), total_games, total_wins, total_losses, total_concessions, win_rate, current_streak, best_streak, average_rating, last_game_date, updated_at
- `UserCardCollection` (`user_card_collection`) - user_id, card_id, quantity, condition, language, is_foil, is_alt_art, acquired_date, acquisition_method, notes, created_at, updated_at
- `CardWishlist` (`card_wishlist`) - user_id, card_id, max_price, notes, created_at
- `UserGroup` (`user_groups`) - name, description, owner_id, is_public, max_members, created_at, updated_at
- `GroupMember` (`group_members`) - group_id, user_id, role, joined_at
- `GroupChatMessage` (`group_chat_messages`) - group_id, sender_id, message, created_at
- `UserNetwork` (`user_networks`) - name, description, creator_id, is_public, created_at
- `NetworkMember` (`network_members`) - network_id, user_id, role, joined_at
- `UserPreference` (`user_preferences`) - user_id (PK), theme, notifications_enabled, email_notifications, auto_save_decks, default_format, language, updated_at
- `UserActivityLog` (`user_activity_log`) - user_id, activity_type, activity_data, ip_address, created_at
**5. User Deck Models (`app/models/user_deck.py`)**
- `UserDeck` (`user_decks`) - user_id, name, status, folder_id, format, notes, is_precedent, precedent_name, created_at, updated_at
- `UserDeckCard` (`user_deck_cards`) - deck_id, card_id, quantity, zone, position
- `DeckPrecedent` (`deck_precedents`) - name, description, format, is_public, created_by, created_at, updated_at
- `DeckPrecedentCard` (`deck_precedent_cards`) - precedent_id, card_id, quantity, zone
- `CardSuggestion` (`card_suggestions`) - deck_id, card_id, source_card_id, suggestion_type, confidence, notes, created_at
**6. Card Import Models (`app/models/card_import.py`)**
- `CardImportBatch` (`card_import_batches`) - user_id, filename, file_type, file_size, status, total_cards, matched_cards, unmatched_cards, match_results, error_message, created_at, updated_at
- `UserCardImportRecord` (`user_card_imports_confirmed`) - user_id, batch_id, is_confirmed, confirmed_at
#### Test Cases:
1. **User Data Models (`app/models/user_data.py`)**
- Verify `User` model matches `mtgonline_users` table
- Verify all columns defined
- Verify relationships defined
- Check for missing fields
2. **User Deck Models (`app/models/user_deck.py`)**
- Verify `DecklistFile`, `DecklistCard`, `DecklistPrecedent`, `DecklistSuggestion` models
- Verify foreign key relationships
- Verify cascade delete behavior
- Check for missing fields
3. **Card Import Models (`app/models/user_card_import.py`)**
- Verify `CardImport` model
- Verify foreign key to `mtgonline_users`
- Check for missing fields
4. **Game Models (`app/models/game.py`)**
- Verify `Game`, `GamePlayer`, `GameCard`, `GameAction`, `GameLog` models
- Verify relationships
- Check for missing fields
5. **MTG Card Models (`app/models/mtg_card.py`)**
- Verify `MtgonlineCard`, `CardPriceHistory` models
- Verify relationships
- Check for missing fields
6. **Model Consistency**
- Verify all models have proper `__tablename__`
- Verify all foreign keys reference correct tables
- Verify all relationships are bidirectional where needed
- Check for missing imports
- Verify model imports in `app/models/__init__.py`
1. **Verify all models have proper `__tablename__`**
2. **Verify all foreign keys reference correct tables**
3. **Verify all relationships are bidirectional where needed**
4. **Verify cascade delete behavior**
5. **Verify indexes on FK columns**
6. **Verify unique constraints**
7. **Verify model imports in `app/models/__init__.py`**
8. **Check for missing fields compared to migration definitions**
**Verification:** All models compile without errors, relationships are correct, no missing fields.
@@ -122,42 +149,82 @@ This test plan is designed for iterative execution using sub-agents, with each s
**Scope:** Pydantic schemas, request/response validation
**Sub-agent Task:** Verify all schema definitions are correct
#### Actual Schema Structure:
**1. Core Schemas (`app/schemas/schemas.py`)**
- **Authentication:** `LoginRequest`, `LoginResponse`, `RefreshTokenRequest`, `TokenResponse`
- **User:** `UserBase`, `UserCreate`, `UserUpdate`, `UserResponse`
- **Deck:** `DeckCreate`, `DeckUpdate`, `DeckResponse`, `FolderCreate`, `FolderResponse`
- **Game:** `GameCreate`, `GameResponse`
- **Room:** `RoomResponse`
- **Ban:** `BanCreate`, `BanResponse`
- **Error:** `ErrorResponse`, `ValidationErrorResponse`
- **Pagination:** `PaginationParams`, `PaginatedResponse`
- **Card Mirror:** `CardMirrorResponse`, `DeckCardLinkResponse`, `DeckWithCardsResponse`
**2. User Data Schemas (`app/schemas/user_data_schemas.py`)**
- **Enums:** `DeckVersionStatus`, `GameReplayStatus`, `GameOutcomeType`, `GroupMemberRole`, `NetworkMemberRole`, `UserPreferenceTheme`, `ActivityType`
- **Session:** `SessionResponse`, `SessionCleanupResponse`
- **Deck Version:** `DeckVersionCreate`, `DeckVersionUpdate`, `DeckVersionResponse`, `DeckVersionListResponse`
- **Game Replay:** `GameReplayCreate`, `GameReplayUpdate`, `GameReplayResponse`, `GameReplayListResponse`
- **Game Outcome:** `GameOutcomeCreate`, `GameOutcomeResponse`, `GameOutcomeListResponse`
- **User Statistics:** `UserStatisticsResponse`, `StatisticsUpdateResponse`
- **Card Collection:** `CardCollectionCreate`, `CardCollectionUpdate`, `CardCollectionResponse`, `CardCollectionListResponse`
- **Wishlist:** `WishlistCreate`, `WishlistUpdate`, `WishlistResponse`, `WishlistListResponse`
- **Group:** `GroupCreate`, `GroupUpdate`, `GroupMemberCreate`, `GroupMemberUpdate`, `GroupMemberRemove`, `GroupResponse`, `GroupListResponse`, `GroupChatMessageCreate`, `GroupChatMessageResponse`, `GroupChatMessageListResponse`
- **Network:** `NetworkCreate`, `NetworkUpdate`, `NetworkMemberCreate`, `NetworkResponse`, `NetworkListResponse`
- **Preference:** `UserPreferenceUpdate`, `UserPreferenceResponse`
- **Activity Log:** `ActivityLogEntry`, `ActivityLogListResponse`
- **Generic:** `MessageResponse`, `CountResponse`, `ErrorDetail`
**3. User Deck Schemas (`app/schemas/user_deck_schemas.py`)**
- **Enums:** `DeckStatus`, `DeckZone`, `SuggestionType`
- **Deck:** `UserDeckCreate`, `UserDeckUpdate`, `UserDeckResponse`, `UserDeckListResponse`
- **Deck Card:** `DeckCardCreate`, `DeckCardUpdate`, `DeckCardResponse`, `DeckCardWithDetailsResponse`, `DeckCardListResponse`
- **Deck Precedent:** `PrecedentCreate`, `PrecedentUpdate`, `PrecedentResponse`, `PrecedentListResponse`
- **Card Suggestion:** `SuggestionCreate`, `SuggestionResponse`, `SuggestionListResponse`
- **Deck Action:** `DeckFinalizeRequest`, `DeckFinalizeResponse`, `DeckDeleteResponse`
- **Search:** `CardSearchRequest`, `CardSearchResponse`
- **Generic:** `MessageResponse`, `CountResponse`
**4. Card Import Schemas (`app/schemas/card_import_schemas.py`)**
- `CardImportRequest`, `CardImportResponse`, `CardImportStatusResponse`
- `CardMatchResult`, `CardImportSummary`
- `MessageResponse`, `CountResponse`, `ErrorResponse`
**5. Card Search Schemas (`app/schemas/card_search_schemas.py`)**
- `CardResponse`, `SetResponse`, `CardTypeResponse`, `CardSearchResponse`
- `CardImportResponse`, `CardImportStatusResponse`
- `CardMatchResult`, `CardImportSummary`
- `MessageResponse`, `CountResponse`, `ErrorResponse`
**6. Protocol Schemas (`app/schemas/proto_messages.py`)**
- **Base:** `ProtoMessageBase`
- **Commands:** `SessionCommand`, `GameCommand`, `GameEvent`, `Response`
- **Server Info:** `ServerInfoUser`, `ServerInfoDeckStorageFile`, `ServerInfoDeckStorageFolder`, `ServerInfoDeckStorageTreeItem`, `ServerInfoCard`, `ServerInfoZone`, `ServerInfoGame`
**7. Protocol Constants (`app/schemas/protocol_constants.py`)**
- `SessionCommandType` (IntEnum)
- `GameCommandType` (IntEnum)
- `GameEventType` (IntEnum)
- `ResponseCode` (IntEnum)
- `ZoneType` (IntEnum)
- `UserLevelFlag` (IntFlag)
**8. User Card Collection Schemas (`app/schemas/user_card_collection.py`)**
- **Enums:** `CardCondition`, `AcquisitionMethod`
- **Card Collection:** `CardCollectionCreate`, `CardCollectionUpdate`, `CardCollectionResponse`, `CardCollectionListResponse`
- **Wishlist:** `WishlistCreate`, `WishlistUpdate`, `WishlistResponse`, `WishlistListResponse`
- **Collection Statistics:** `CollectionStatistics`, `CollectionSummaryResponse`
- **Generic:** `MessageResponse`, `CountResponse`, `ErrorDetail`
#### Test Cases:
1. **User Data Schemas (`app/schemas/user_data_schemas.py`)**
- Verify `UserCreate`, `UserUpdate`, `UserResponse` schemas
- Verify all required fields
- Check for missing validation
2. **User Deck Schemas (`app/schemas/user_deck_schemas.py`)**
- Verify `DecklistFileCreate`, `DecklistFileUpdate`, `DecklistFileResponse`
- Verify `DecklistCardCreate`, `DecklistCardUpdate`, `DecklistCardResponse`
- Verify `DecklistPrecedentCreate`, `DecklistPrecedentUpdate`, `DecklistPrecedentResponse`
- Verify `DecklistSuggestionCreate`, `DecklistSuggestionUpdate`, `DecklistSuggestionResponse`
- Check for missing fields
3. **Card Import Schemas (`app/schemas/card_import_schemas.py`)**
- Verify `CardImportCreate`, `CardImportResponse`
- Verify `CardImportStatusResponse`, `CardImportSummaryResponse`
- Check for missing fields
4. **Game Schemas (`app/schemas/game_schemas.py`)**
- Verify `GameCreate`, `GameUpdate`, `GameResponse`
- Verify `GamePlayerCreate`, `GamePlayerResponse`
- Verify `GameCardCreate`, `GameCardResponse`
- Verify `GameActionCreate`, `GameActionResponse`
- Verify `GameLogCreate`, `GameLogResponse`
- Check for missing fields
5. **MTG Card Schemas (`app/schemas/mtg_card_schemas.py`)**
- Verify `MtgonlineCardCreate`, `MtgonlineCardUpdate`, `MtgonlineCardResponse`
- Verify `CardPriceHistoryCreate`, `CardPriceHistoryResponse`
- Check for missing fields
6. **Schema Consistency**
- Verify all schemas have proper `model_config`
- Verify required vs optional fields
- Check for missing imports
- Verify schema imports in `app/schemas/__init__.py`
1. **Verify all schemas have proper `model_config`**
2. **Verify required vs optional fields**
3. **Verify validation rules (min/max length, patterns, etc.)**
4. **Verify schema imports in `app/schemas/__init__.py`**
5. **Check for missing fields compared to model definitions**
6. **Verify enum values match expected constants**
**Verification:** All schemas compile without errors, validation rules are correct, no missing fields.
@@ -167,42 +234,63 @@ This test plan is designed for iterative execution using sub-agents, with each s
**Scope:** FastAPI routers, endpoint definitions, dependencies
**Sub-agent Task:** Verify all router definitions are correct
#### Actual Router Structure:
**1. Auth Router (`app/routers/auth.py`)**
- `POST /login` - Authenticate user, return JWT tokens
- `POST /refresh` - Refresh access token
- `POST /register` - Register new user
- `GET /me` - Get current authenticated user
**2. Users Router (`app/routers/users.py`)**
- `GET /{user_id}` - Get user by ID
- `PATCH /{user_id}` - Update user profile
- `POST /{user_id}/ban` - Ban user (admin only)
- `POST /{user_id}/unban` - Unban user (admin only)
**3. Decks Router (`app/routers/decks.py`)**
- **Deck CRUD:**
- `GET /` - List user's decks with filtering
- `POST /` - Create new user deck (DRAFT)
- `GET /{deck_id}` - Get specific deck
- `PATCH /{deck_id}` - Update deck
- `DELETE /{deck_id}` - Delete deck
- **Deck Finalize:**
- `POST /{deck_id}/finalize` - Transition DRAFT to FINAL
- **Deck Card Management:**
- `POST /{deck_id}/cards` - Add card to deck
- `GET /{deck_id}/cards` - Get all cards in deck
- `PATCH /{deck_id}/cards/{card_id}` - Update card in deck
- `DELETE /{deck_id}/cards/{card_id}` - Remove card from deck
- **Deck Precedents:**
- `GET /precedents` - List available precedents
- `POST /precedents` - Create precedent (template)
- `GET /precedents/{precedent_id}` - Get specific precedent
- `POST /precedents/{precedent_id}/use` - Clone precedent to new deck
- **Card Search:**
- `POST /search/cards` - Search MTG cards
- **Card Suggestions:**
- `GET /{deck_id}/suggestions` - Get card suggestions
- `POST /{deck_id}/suggestions` - Add suggestion
**4. Additional Routers (exist but not fully documented)**
- `app/routers/rooms.py` - Rooms router
- `app/routers/games/` - Games router (directory)
- `app/routers/admin.py` - Admin router
- `app/routers/card_router.py` - Card router
- `app/routers/interactions.py` - Interactions router
- `app/routers/refresh.py` - Refresh router
- `app/routers/card_import.py` - Card import router
- `app/routers/ws.py` - WebSocket router
#### Test Cases:
1. **User Data Router (`app/routers/user_data.py`)**
- Verify all endpoints defined
- Verify request/response schemas
- Verify dependencies (auth, etc.)
- Check for missing endpoints
2. **Deck Router (`app/routers/decks.py`)**
- Verify all endpoints defined
- Verify request/response schemas
- Verify dependencies
- Check for missing endpoints
3. **Card Import Router (`app/routers/card_import.py`)**
- Verify all endpoints defined
- Verify request/response schemas
- Verify dependencies
- Check for missing endpoints
4. **Game Router (`app/routers/game.py`)**
- Verify all endpoints defined
- Verify request/response schemas
- Verify dependencies
- Check for missing endpoints
5. **MTG Card Router (`app/routers/mtg_card.py`)**
- Verify all endpoints defined
- Verify request/response schemas
- Verify dependencies
- Check for missing endpoints
6. **Router Consistency**
- Verify all routers imported in `app/main.py`
- Verify prefix paths are correct
- Verify tag assignments
- Check for missing imports
1. **Verify all endpoints defined with correct HTTP methods**
2. **Verify request/response schemas match**
3. **Verify dependencies (auth, db session, etc.)**
4. **Verify prefix paths are correct**
5. **Verify tag assignments**
6. **Verify all routers imported in `app/main.py`**
7. **Check for missing endpoints**
**Verification:** All routers compile without errors, endpoints are properly defined, no missing imports.
@@ -212,36 +300,40 @@ This test plan is designed for iterative execution using sub-agents, with each s
**Scope:** Business logic, service functions
**Sub-agent Task:** Verify all service implementations are correct
#### Actual Service Structure:
**1. Card Services**
- `app/services/card_database.py` - Card database management
- `app/services/card_mirror_service.py` - Card mirroring
- `app/services/card_search_service.py` - Card search
- `app/services/fuzzy_card_matcher.py` - Fuzzy card matching
**2. Deck Services**
- `app/services/deck_manager.py` - Deck management
- `app/services/deck_parser.py` - Deck parsing
- `app/services/deck_suggestion_service.py` - Deck suggestions
**3. File Services**
- `app/services/file_parser.py` - File parsing
**4. Game Services**
- `app/services/game_server.py` - Game server logic
**5. Import Services**
- `app/services/import_batch_processor.py` - Import batch processing
**6. MTGJSON Services**
- `app/services/mtgjson_downloader.py` - MTGJSON data download
- `app/services/mtgjson_loader.py` - MTGJSON data loading
- `app/services/mtgjson_manager.py` - MTGJSON data management
- `app/services/mtgjson_uploader.py` - MTGJSON data upload
#### Test Cases:
1. **User Service (`app/services/user_service.py`)**
- Verify all functions defined
- Verify function signatures
- Check for missing implementations
2. **Deck Service (`app/services/deck_service.py`)**
- Verify all functions defined
- Verify function signatures
- Check for missing implementations
3. **Card Import Service (`app/services/card_import_service.py`)**
- Verify all functions defined
- Verify function signatures
- Check for missing implementations
4. **Game Service (`app/services/game_service.py`)**
- Verify all functions defined
- Verify function signatures
- Check for missing implementations
5. **MTG Card Service (`app/services/mtg_card_service.py`)**
- Verify all functions defined
- Verify function signatures
- Check for missing implementations
6. **Service Consistency**
- Verify all services imported where needed
- Verify function calls match implementations
- Check for missing imports
1. **Verify all functions defined with proper signatures**
2. **Verify function implementations match expected behavior**
3. **Verify service imports (models, schemas, utilities)**
4. **Check for missing implementations**
5. **Verify error handling**
**Verification:** All services compile without errors, functions are properly implemented, no missing imports.
@@ -251,31 +343,26 @@ This test plan is designed for iterative execution using sub-agents, with each s
**Scope:** Utility functions, helpers, constants
**Sub-agent Task:** Verify all utility implementations are correct
#### Actual Utility Structure:
**1. Core Configuration (`app/core/`)**
- `app/core/database.py` - Database configuration, session management
- `app/core/redis_client.py` - Redis client setup
- `app/core/security.py` - JWT tokens, password hashing, auth dependencies
- `app/core/settings.py` - Application settings, environment variables
**2. Utilities (`app/utils/`)**
- `app/utils/auth.py` - Authentication utilities
- `app/utils/database.py` - Database utilities
- `app/utils/errors.py` - Custom exceptions and error handlers
- `app/utils/constants.py` - Application constants
#### Test Cases:
1. **Auth Utilities (`app/utils/auth.py`)**
- Verify all functions defined
- Verify function signatures
- Check for missing implementations
2. **Database Utilities (`app/utils/database.py`)**
- Verify all functions defined
- Verify function signatures
- Check for missing implementations
3. **Error Handlers (`app/utils/errors.py`)**
- Verify all exception classes defined
- Verify error codes
- Check for missing exceptions
4. **Constants (`app/utils/constants.py`)**
- Verify all constants defined
- Verify constant values
- Check for missing constants
5. **Utility Consistency**
- Verify all utilities imported where needed
- Verify function calls match implementations
- Check for missing imports
1. **Verify all functions defined with proper signatures**
2. **Verify function implementations**
3. **Verify exception classes defined with proper error codes**
4. **Verify constants defined with correct values**
5. **Verify utility imports where needed**
**Verification:** All utilities compile without errors, functions are properly implemented, no missing imports.
@@ -286,26 +373,15 @@ This test plan is designed for iterative execution using sub-agents, with each s
**Sub-agent Task:** Verify all configuration is correct
#### Test Cases:
1. **Settings (`app/core/settings.py`)**
- Verify all settings defined
- Verify default values
- Check for missing settings
2. **Database Configuration (`app/core/database.py`)**
- Verify database URL configuration
- Verify async/sync engine setup
- Check for missing configuration
3. **App Configuration (`app/main.py`)**
- Verify FastAPI app initialization
- Verify middleware setup
- Verify CORS configuration
- Check for missing configuration
4. **Environment Consistency**
- Verify all settings used in code
- Verify environment variables match settings
- Check for missing configuration
1. **Verify all settings defined in `app/core/settings.py`**
2. **Verify default values are sensible**
3. **Verify database URL configuration**
4. **Verify async/sync engine setup in `app/core/database.py`**
5. **Verify FastAPI app initialization in `app/main.py`**
6. **Verify middleware setup**
7. **Verify CORS configuration**
8. **Verify all settings used in code match defined settings**
9. **Verify environment variables match settings**
**Verification:** All configuration compiles without errors, settings are properly defined, no missing configuration.
@@ -332,7 +408,7 @@ This test plan is designed for iterative execution using sub-agents, with each s
- Check for integration issues
4. **Database-Model Integration**
- Verify models match database schema
- Verify models match database schema (from migrations)
- Verify migrations create correct tables
- Check for integration issues
@@ -349,7 +425,7 @@ This test plan is designed for iterative execution using sub-agents, with each s
## Execution Strategy
### Sub-Agent Execution Order:
1. **Phase 1:** Database & Migration Tests
1. **Phase 1:** Database & Migration Tests (COMPLETE)
2. **Phase 2:** Model Layer Tests
3. **Phase 3:** Schema Layer Tests
4. **Phase 4:** Router Layer Tests
@@ -385,3 +461,6 @@ Each sub-agent should report:
## Summary
This test plan provides a systematic approach to verifying the entire backend system by breaking it down into 8 manageable phases. Each phase can be executed by a sub-agent independently, ensuring comprehensive coverage while staying within context limits. The plan focuses on consistency, correctness, and completeness of the codebase.
**Current Status:** Phase 1 Complete
**Next Phase:** Phase 2 - Model Layer Tests
File diff suppressed because it is too large Load Diff
+313
View File
@@ -0,0 +1,313 @@
# Phase 2 - Model Layer Test Report
**Date:** 2026-07-23
**Scope:** SQLAlchemy model definitions vs. Alembic migration schema
**Status:** ❌ FAIL (2 Critical, 3 Major, 4 Minor issues)
---
## Summary
| Category | Count |
|----------|-------|
| Critical | 2 |
| Major | 3 |
| Minor | 4 |
| **Total Issues** | **9** |
| Models Verified | 28 classes across 7 files |
| Migrations Verified | 6 files (000005) |
| Tables Verified | 27 tables |
**Overall: FAIL** — Two critical issues will prevent the application from starting:
1. A circular import between `models.py` and `mirror_models.py` will cause `ImportError` at runtime
2. Migration `005` imports models to extract column definitions, triggering the same circular import
---
## Issues Found
### CRITICAL
#### Issue #1: Circular Import — `models.py` ↔ `mirror_models.py`
- **File:** `app/models/models.py` (line 209) and `app/models/mirror_models.py` (line 100)
- **Severity:** Critical
- **Description:** `models.py` imports `MtgCardMirror` and `DeckCardLink` from `mirror_models.py` at the top of the file. `mirror_models.py` imports `DecklistFile` from `models.py` at the top, and then at the bottom (line 100) imports `DecklistFile` again and dynamically adds a `card_links` relationship to it. This creates a circular import chain:
```
models.py → mirror_models.py → models.py (circular!)
```
- **Impact:** Any code that imports from either `models.py` or `mirror_models.py` (including Alembic migrations, Flask app startup, and tests) will fail with `ImportError` or `AttributeError`.
- **Recommendation:** Restructure the import. Move the dynamic `DecklistFile.card_links` relationship addition to a separate initialization file (e.g., `app/models/relationships.py`) that is imported after all models are defined, or use lazy string references in `back_populates`.
#### Issue #2: Migration `005_missing_tables.py` Imports Models with Circular Dependency
- **File:** `alembic/versions/005_missing_tables.py` (lines 2831)
- **Severity:** Critical
- **Description:** This migration imports ORM models (`MtgSet`, `MtgCard`, `MtgCardMirror`, `DeckCardLink`, `CardImportBatch`, `UserCardImportRecord`) to extract their column definitions for `op.create_table()`. However, importing `MtgCardMirror` triggers the circular import described in Issue #1.
```python
from app.models.mtg_models import MtgSet, MtgCard
from app.models.mirror_models import MtgCardMirror, DeckCardLink
from app.models.card_import_batch import CardImportBatch
from app.models.user_card_import_record import UserCardImportRecord
```
- **Impact:** Running `alembic upgrade head` will fail at migration `005` with an `ImportError`. The database cannot be brought online.
- **Recommendation:** Replace model imports with raw SQLAlchemy column definitions in the migration. Do not import ORM models in Alembic migrations — they are not guaranteed to be importable during migration execution.
---
### MAJOR
#### Issue #3: Orphaned Migration `004_card_import_table.py` — No Corresponding Model
- **File:** `alembic/versions/004_card_import_table.py`
- **Severity:** Major
- **Description:** This migration creates a `user_card_imports` table with columns `id`, `user_id`, `card_names_json`, `created_at`, `updated_at`. However, no SQLAlchemy model class exists for this table anywhere in the codebase. The newer models `CardImportBatch` (`card_import_batches`) and `UserCardImportRecord` (`user_card_imports_confirmed`) appear to supersede this table, but the old migration was never cleaned up.
- **Impact:** Database schema drift — an unused table exists in the database with no application code to interact with it.
- **Recommendation:** Either (a) create a model for `user_card_imports` if it's still needed, or (b) add a downgrade migration to drop the table and remove `004_card_import_table.py`.
#### Issue #4: `MtgCardMirror.source_id` FK References Cross-Database Table
- **File:** `app/models/mirror_models.py` (line 33)
- **Severity:** Major
- **Description:** `MtgCardMirror.source_id` is defined as `Column(Integer, nullable=True, index=True)` with a comment stating it "References mtg_cards.id". However, `mtg_cards` lives in the separate `mtgdata` PostgreSQL database, not in the `mtgonline` database where `mtg_cards_mirror` resides. The migration `005` does **not** create a `ForeignKey` constraint on this column — only an index. The model also lacks a `ForeignKey` definition.
- **Impact:** No referential integrity enforcement. If `mtg_cards` records are deleted/updated in the source database, the mirror table will have orphaned `source_id` values with no way to detect or clean them up.
- **Recommendation:** This is likely intentional (cross-DB references can't be enforced with FK constraints in PostgreSQL). Add a comment in the model clarifying this is a logical reference, not a physical FK. Consider adding a periodic sync validation job.
#### Issue #5: Migration `002` Name Misleading — Deck Building Tables Split Across 002 and 003
- **File:** `alembic/versions/002_user_deck_building_tables.py` and `003_mtgonline_cards_table.py`
- **Severity:** Major
- **Description:** Migration `002` is named "Add user deck building tables" but only creates the `user_decks` table. Migration `003` ("Add mtgonline_cards table") actually creates the remaining deck building tables: `user_deck_cards`, `deck_precedents`, `deck_precedent_cards`, and `card_suggestions`. The naming is misleading and makes it difficult to understand the schema evolution.
- **Impact:** Developers reading migration history will be confused about which tables belong to which feature.
- **Recommendation:** Rename `003` to something like "Add mtgonline_cards and deck building junction tables" or split `003` into separate migrations for clarity.
---
### MINOR
#### Issue #6: `MtonlineCard` Typo in Class Name
- **File:** `app/models/models.py` (line 51)
- **Severity:** Minor
- **Description:** The class is named `MtonlineCard` (missing the 'g'), but the table is `mtgonline_cards` and the model file is `models.py`. The correct name should be `MtgonlineCard`. This typo is used consistently throughout the codebase (e.g., in `user_deck.py` line 18), so changing it would require updating all references.
- **Impact:** Code readability and consistency. No functional impact since the `__tablename__` is correct.
- **Recommendation:** Rename to `MtgonlineCard` across all files (`models.py`, `user_deck.py`, `__init__.py`).
#### Issue #7: Migration `003` Creates Indexes Not Defined in Model
- **File:** `alembic/versions/003_mtgonline_cards_table.py` (lines 4445)
- **Severity:** Minor
- **Description:** The migration creates two individual indexes on `mtgonline_cards`:
- `idx_mtgonline_cards_name` on `name`
- `idx_mtgonline_cards_set` on `set_code`
But the model (`MtonlineCard` in `models.py`) does not define these as SQLAlchemy `Index` objects. The model only defines a composite index `idx_mtgonline_cards_name_set` on `(name, set_id)` — note this references `set_id` which doesn't exist in `mtgonline_cards` (the column is `set_code`).
- **Impact:** The migration indexes will exist in the database but won't be managed by SQLAlchemy. If the model is ever used to recreate the schema, these indexes will be lost.
- **Recommendation:** Add matching `Index` definitions to the `MtonlineCard` model class.
#### Issue #8: `UserDeck.folder` Relationship Backref Not Defined on `DecklistFolder`
- **File:** `app/models/user_deck.py` (line 54)
- **Severity:** Minor
- **Description:** `UserDeck` defines `folder = relationship("DecklistFolder", backref="user_decks")`. However, `DecklistFolder` in `models.py` does not define a corresponding `user_decks` relationship or backref. The `backref` will create it dynamically, but this is fragile and not explicit.
- **Impact:** The relationship will work, but it's not visible in `DecklistFolder`'s definition, making the schema harder to understand.
- **Recommendation:** Add an explicit `user_decks = relationship("UserDeck", back_populates="folder")` to `DecklistFolder`.
#### Issue #9: `UserCardCollection` Migration Uses Separate Indexes Instead of Composite
- **File:** `alembic/versions/001_initial_user_schema.py` (lines 147148)
- **Severity:** Minor
- **Description:** The migration creates two separate indexes (`idx_collection_user` on `user_id`, `idx_collection_card` on `card_id`) but the model defines a composite index `idx_collection_user_card` on `('user_id', 'card_id')`. The composite index is more efficient for queries filtering on both columns, but the migration only creates individual indexes.
- **Impact:** Slightly suboptimal query performance. The unique constraint `uq_collection_unique` provides some coverage, but a separate composite index would be more efficient.
- **Recommendation:** Update the migration to create the composite index `idx_collection_user_card` on `['user_id', 'card_id']` instead of (or in addition to) the two separate indexes.
---
## Verified Items (Passed)
### Core Models (`models.py`) — All 8 models verified ✅
| Model | Table | `__tablename__` | FKs | Relationships | Indexes | Unique Constraints |
|-------|-------|-----------------|-----|---------------|---------|-------------------|
| `User` | `mtgonline_users` | ✅ | — | ✅ (decklist_files, decklist_folders) | ✅ (username, email) | ✅ (username) |
| `MtonlineCard` | `mtgonline_cards` | ✅ | — | — | ⚠️ (Issue #7) | — |
| `DecklistFolder` | `mtgonline_decklist_folders` | ✅ | ✅ (owner_id, parent_id) | ✅ (owner, children, parent, files) | — | — |
| `DecklistFile` | `mtgonline_decklist_files` | ✅ | ✅ (folder_id, owner_id) | ✅ (folder, owner) | ✅ (idx_decks_owner, idx_decks_folder) | — |
| `Room` | `mtgonline_rooms` | ✅ | — | ✅ (game_types) | ✅ (name unique) | ✅ (name) |
| `RoomGameType` | `mtgonline_rooms_gametypes` | ✅ | ✅ (room_id) | ✅ (room) | — | — |
| `Ban` | `mtgonline_bans` | ✅ | ✅ (user_id) | ✅ (user) | ✅ (idx_bans_active) | — |
| `GameLog` | `mtgonline_log` | ✅ | ✅ (room_id, player_id) | ✅ (room, player) | ✅ (idx_log_timestamp) | — |
| `AuditLog` | `mtgonline_audit` | ✅ | ✅ (admin_id, target_user_id) | ✅ (admin, target_user) | — | — |
### MTG Models (`mtg_models.py`) — Both models verified ✅
| Model | Table | `__tablename__` | FKs | Relationships | Indexes | Unique Constraints |
|-------|-------|-----------------|-----|---------------|---------|-------------------|
| `MtgSet` | `mtg_sets` | ✅ | — | ✅ (cards) | ✅ (code unique, index) | ✅ (code) |
| `MtgCard` | `mtg_cards` | ✅ | ✅ (set_id → mtg_sets.id) | ✅ (set) | ✅ (name, mana_cost, type_line, rarity, composite) | — |
### Mirror Models (`mirror_models.py`) — Both models verified ✅
| Model | Table | `__tablename__` | FKs | Relationships | Indexes | Unique Constraints |
|-------|-------|-----------------|-----|---------------|---------|-------------------|
| `MtgCardMirror` | `mtg_cards_mirror` | ✅ | ⚠️ (source_id, Issue #4) | ✅ (deck_links) | ✅ (source_id, name, set_code) | — |
| `DeckCardLink` | `deck_card_links` | ✅ | ✅ (deck_id, card_id) | ✅ (deck, card) | ✅ (idx_deck_card_deck, idx_deck_card_card) | ✅ (uq_deck_card_link) |
### User Data Models (`user_data.py`) — All 14 models verified ✅
| Model | Table | `__tablename__` | PK Type | FKs | Unique Constraints |
|-------|-------|-----------------|---------|-----|-------------------|
| `UserSession` | `user_sessions` | ✅ | BigInteger | ✅ (user_id) | ✅ (session_token_hash) |
| `DeckVersion` | `deck_versions` | ✅ | BigInteger | ✅ (deck_id) | — |
| `GameReplay` | `game_replays` | ✅ | BigInteger | ✅ (room_id) | ✅ (game_uuid) |
| `ReplayPlayer` | `replay_players` | ✅ | BigInteger | ✅ (replay_id, user_id, deck_id) | — |
| `GameOutcome` | `game_outcomes` | ✅ | BigInteger | ✅ (user_id, game_uuid, opponent_id) | — |
| `UserStatistics` | `user_statistics` | ✅ | Integer (PK) | ✅ (user_id as PK) | — |
| `UserCardCollection` | `user_card_collection` | ✅ | BigInteger | ✅ (user_id) | ✅ (uq_collection_unique) |
| `CardWishlist` | `card_wishlist` | ✅ | BigInteger | ✅ (user_id) | ✅ (uq_wishlist_user_card) |
| `UserGroup` | `user_groups` | ✅ | BigInteger | ✅ (owner_id) | — |
| `GroupMember` | `group_members` | ✅ | BigInteger | ✅ (group_id, user_id) | ✅ (uq_group_member) |
| `GroupChatMessage` | `group_chat_messages` | ✅ | BigInteger | ✅ (group_id, sender_id) | — |
| `UserNetwork` | `user_networks` | ✅ | BigInteger | ✅ (creator_id) | — |
| `NetworkMember` | `network_members` | ✅ | BigInteger | ✅ (network_id, user_id) | ✅ (uq_network_member) |
| `UserPreference` | `user_preferences` | ✅ | Integer (PK) | ✅ (user_id as PK) | — |
| `UserActivityLog` | `user_activity_log` | ✅ | BigInteger | ✅ (user_id) | — |
### User Deck Models (`user_deck.py`) — All 5 models verified ✅
| Model | Table | `__tablename__` | FKs | Unique Constraints |
|-------|-------|-----------------|-----|-------------------|
| `UserDeck` | `user_decks` | ✅ | ✅ (user_id, folder_id) | — |
| `UserDeckCard` | `user_deck_cards` | ✅ | ✅ (deck_id, card_id) | ✅ (uq_deck_card_unique) |
| `DeckPrecedent` | `deck_precedents` | ✅ | ✅ (created_by) | — |
| `DeckPrecedentCard` | `deck_precedent_cards` | ✅ | ✅ (precedent_id, card_id) | ✅ (uq_precedent_card_unique) |
| `CardSuggestion` | `card_suggestions` | ✅ | ✅ (deck_id, card_id, source_card_id) | ✅ (uq_suggestion_unique) |
### Card Import Models — Both models verified ✅
| Model | Table | `__tablename__` | FKs |
|-------|-------|-----------------|-----|
| `CardImportBatch` | `card_import_batches` | ✅ | ✅ (user_id) |
| `UserCardImportRecord` | `user_card_imports_confirmed` | ✅ | ✅ (user_id, batch_id) |
### Model Exports (`__init__.py`) — Verified ✅
All 34 model classes are properly exported in `__all__` and importable from `app.models`.
### Migration Chain — Verified ✅
```
000 (base_tables) → 001 (initial_user_schema) → 002 (user_deck_building) → 003 (mtgonline_cards) → 004 (card_import) → 005 (missing_tables)
```
All `down_revision` links are correct. All `upgrade()` and `downgrade()` functions are properly defined.
### Cascade Delete Behavior — Verified ✅
| Relationship | Cascade | Correct? |
|-------------|---------|----------|
| `User.decklist_files` | `all, delete-orphan` | ✅ |
| `User.decklist_folders` | `all, delete-orphan` | ✅ |
| `DecklistFolder.children` | `all, delete-orphan` | ✅ |
| `DecklistFolder.files` | `all, delete-orphan` | ✅ |
| `Room.game_types` | `all, delete-orphan` | ✅ |
| `MtgCardMirror.deck_links` | `all, delete-orphan` | ✅ |
| `GameReplay.players` | `all, delete-orphan` | ✅ |
| `GameReplay.outcomes` | `all, delete-orphan` | ✅ |
| `UserGroup.members` | `all, delete-orphan` | ✅ |
| `UserGroup.messages` | `all, delete-orphan` | ✅ |
| `UserNetwork.members` | `all, delete-orphan` | ✅ |
| `UserDeck.cards` | `all, delete-orphan` | ✅ |
| `DeckPrecedent.cards` | `all, delete-orphan` | ✅ |
| FK `ondelete="CASCADE"` | Used on UserSession, DeckVersion, ReplayPlayer, UserCardCollection, CardWishlist, UserDeck, UserDeckCard, DeckPrecedentCard, CardImportBatch, UserCardImportRecord, DeckCardLink | ✅ |
---
## Recommendations
### Immediate (Blockers)
1. **Fix circular import** between `models.py` and `mirror_models.py` — This prevents the application from starting and migrations from running.
2. **Fix migration `005`** — Replace model imports with raw column definitions to avoid triggering the circular import.
### Short-Term
3. **Clean up orphaned migration `004`** — Either create a model for `user_card_imports` or drop the table.
4. **Rename `MtonlineCard` → `MtgonlineCard`** — Fix the typo for code consistency.
5. **Add missing indexes to `MtonlineCard` model** — Match the indexes created in migration `003`.
### Long-Term
6. **Add explicit backref on `DecklistFolder`** for `UserDeck.folder` relationship.
7. **Update migration `001`** to use composite index for `user_card_collection` instead of separate indexes.
8. **Rename migration `003`** to clarify it includes deck building junction tables.
9. **Document cross-DB reference** for `MtgCardMirror.source_id` — Add a comment clarifying it's a logical (not physical) FK.
---
## Appendix: Column-by-Column Comparison
### `mtgonline_users` (User) — Migration 000 vs Model
| Column | Migration | Model | Match |
|--------|-----------|-------|-------|
| id | Integer PK | Integer PK ✅ |
| username | String(64) unique nullable=False index | String(64) unique nullable=False index ✅ |
| password_hash | String(128) nullable=False | String(128) nullable=False ✅ |
| salt | String(128) nullable=False | String(128) nullable=False ✅ |
| email | String(255) nullable=True index | String(255) nullable=True index ✅ |
| country | String(2) nullable=True | String(2) nullable=True ✅ |
| real_name | String(128) nullable=True | String(128) nullable=True ✅ |
| avatar_bmp | Text nullable=True | Text nullable=True ✅ |
| privlevel | String(50) server_default='User' | String(50) default="User" ⚠️ |
| is_active | Boolean default=True | Boolean default=True ✅ |
| is_banned | Boolean default=False | Boolean default=False ✅ |
| ban_reason | Text nullable=True | Text nullable=True ✅ |
| ban_ends | DateTime nullable=True | DateTime nullable=True ✅ |
| vip_status | Integer default=0 | Integer default=0 ✅ |
| vip_expiry | DateTime nullable=True | DateTime nullable=True ✅ |
| creation_date | DateTime server_default=now() | DateTime server_default=now() ✅ |
| last_login | DateTime nullable=True | DateTime nullable=True ✅ |
> ⚠️ `privlevel`: Migration uses `server_default='User'` (DB-level default), model uses `default="User"` (Python-level default). Both work but `server_default` is preferred for PostgreSQL.
### `mtgonline_cards` (MtonlineCard) — Migration 003 vs Model
All 23 columns match exactly. Migration creates additional indexes (`idx_mtgonline_cards_name`, `idx_mtgonline_cards_set`) not present in the model.
### `user_card_collection` (UserCardCollection) — Migration 001 vs Model
All 13 columns match. Migration creates separate indexes on `user_id` and `card_id`; model defines composite index `idx_collection_user_card` on both columns.
### `card_wishlist` (CardWishlist) — Migration 001 vs Model
All 5 columns match. Unique constraint `uq_wishlist_user_card` on `(user_id, card_id)` matches.
### `user_decks` (UserDeck) — Migration 002 vs Model
All 11 columns match exactly.
### `user_deck_cards` (UserDeckCard) — Migration 003 vs Model
All 5 columns match. Unique constraint `uq_deck_card_unique` on `(deck_id, card_id, zone)` matches.
### `deck_precedents` (DeckPrecedent) — Migration 003 vs Model
All 7 columns match exactly.
### `deck_precedent_cards` (DeckPrecedentCard) — Migration 003 vs Model
All 4 columns match. Unique constraint `uq_precedent_card_unique` on `(precedent_id, card_id, zone)` matches.
### `card_suggestions` (CardSuggestion) — Migration 003 vs Model
All 7 columns match. Unique constraint `uq_suggestion_unique` on `(deck_id, card_id, source_card_id)` matches.
### `mtg_sets` (MtgSet) — Migration 005 vs Model
All 15 columns match exactly.
### `mtg_cards` (MtgCard) — Migration 005 vs Model
All 17 columns match. Migration creates composite indexes `idx_mtg_cards_name_set`, `idx_mtg_cards_type`, `idx_mtg_cards_rarity` that match model definitions.
### `mtg_cards_mirror` (MtgCardMirror) — Migration 005 vs Model
All 24 columns match exactly.
### `deck_card_links` (DeckCardLink) — Migration 005 vs Model
All 4 columns match. Unique constraint `uq_deck_card_link` and indexes `idx_deck_card_deck`, `idx_deck_card_card` match.
### `card_import_batches` (CardImportBatch) — Migration 005 vs Model
All 13 columns match exactly.
### `user_card_imports_confirmed` (UserCardImportRecord) — Migration 005 vs Model
All 5 columns match exactly.
@@ -138,8 +138,7 @@ def upgrade() -> None:
sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()),
sa.Column('updated_at', sa.DateTime(), server_default=sa.func.now(), onupdate=sa.func.now()),
)
op.create_index('idx_collection_user', 'user_card_collection', ['user_id'])
op.create_index('idx_collection_card', 'user_card_collection', ['card_id'])
op.create_index('idx_collection_user_card', 'user_card_collection', ['user_id', 'card_id'])
op.create_unique_constraint('uq_collection_unique', 'user_card_collection', ['user_id', 'card_id', 'is_foil', 'is_alt_art'])
# 9. Card Wishlist Table
@@ -1,4 +1,4 @@
"""Add mtgonline_cards table for local card data mirror
"""Add mtgonline_cards table and deck building junction tables
Revision ID: 003
Revises: 002
+13 -25
View File
@@ -4,13 +4,13 @@ Revision ID: 005
Revises: 004
Create Date: 2026-01-05 00:00:00.000000
This migration creates the following tables, importing model definitions
from their respective source files to ensure column-level accuracy:
This migration creates the following tables using raw column definitions
to avoid circular import issues with the ORM models.
- mtg_sets, mtg_cards → app/models/mtg_models.py
- mtg_cards_mirror, deck_card_links → app/models/mirror_models.py
- card_import_batches → app/models/card_import_batch.py
- user_card_imports_confirmed → app/models/user_card_import_record.py
- mtg_sets, mtg_cards → raw definitions (mirrors mtg_models.py)
- mtg_cards_mirror, deck_card_links → raw definitions (mirrors mirror_models.py)
- card_import_batches → raw definitions (mirrors card_import_batch.py)
- user_card_imports_confirmed → raw definitions (mirrors user_card_import_record.py)
"""
from typing import Sequence, Union
@@ -18,15 +18,6 @@ from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# ---------------------------------------------------------------------------
# Model imports one import per source file so column definitions stay
# synchronised with the ORM classes.
# ---------------------------------------------------------------------------
from app.models.mtg_models import MtgSet, MtgCard
from app.models.mirror_models import MtgCardMirror, DeckCardLink
from app.models.card_import_batch import CardImportBatch
from app.models.user_card_import_record import UserCardImportRecord
# revision identifiers, used by Alembic.
revision: str = '005'
down_revision: Union[str, None] = '004'
@@ -35,14 +26,11 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Create missing tables for MTG data, card imports, and card mirrors.
Column definitions are taken directly from the ORM models imported above.
"""
"""Create missing tables for MTG data, card imports, and card mirrors."""
# ------------------------------------------------------------------
# 1. MTG Sets Table (mtg_sets)
# Source: app/models/mtg_models.py class MtgSet
# Mirrors: app/models/mtg_models.py class MtgSet
# ------------------------------------------------------------------
op.create_table(
'mtg_sets',
@@ -66,7 +54,7 @@ def upgrade() -> None:
# ------------------------------------------------------------------
# 2. MTG Cards Table (mtg_cards)
# Source: app/models/mtg_models.py class MtgCard
# Mirrors: app/models/mtg_models.py class MtgCard
# ------------------------------------------------------------------
op.create_table(
'mtg_cards',
@@ -97,7 +85,7 @@ def upgrade() -> None:
# ------------------------------------------------------------------
# 3. MTG Cards Mirror Table (mtg_cards_mirror)
# Source: app/models/mirror_models.py class MtgCardMirror
# Mirrors: app/models/mirror_models.py class MtgCardMirror
# ------------------------------------------------------------------
op.create_table(
'mtg_cards_mirror',
@@ -129,7 +117,7 @@ def upgrade() -> None:
# ------------------------------------------------------------------
# 4. Card Import Batches Table (card_import_batches)
# Source: app/models/card_import_batch.py class CardImportBatch
# Mirrors: app/models/card_import_batch.py class CardImportBatch
# ------------------------------------------------------------------
op.create_table(
'card_import_batches',
@@ -154,7 +142,7 @@ def upgrade() -> None:
# ------------------------------------------------------------------
# 5. User Card Imports Confirmed Table (user_card_imports_confirmed)
# Source: app/models/user_card_import_record.py
# Mirrors: app/models/user_card_import_record.py
# class UserCardImportRecord
# ------------------------------------------------------------------
op.create_table(
@@ -172,7 +160,7 @@ def upgrade() -> None:
# ------------------------------------------------------------------
# 6. Deck Card Links Table (deck_card_links)
# Source: app/models/mirror_models.py class DeckCardLink
# Mirrors: app/models/mirror_models.py class DeckCardLink
# ------------------------------------------------------------------
op.create_table(
'deck_card_links',
+6
View File
@@ -11,6 +11,11 @@ from app.models.user_data import (
from app.models.user_deck import UserDeck, UserDeckCard, DeckPrecedent, DeckPrecedentCard, CardSuggestion
from app.models.card_import_batch import CardImportBatch
from app.models.user_card_import_record import UserCardImportRecord
from app.models.user_card_import import UserCardImport
# Import relationships LAST to avoid circular imports
# This defines cross-model references (e.g., DecklistFile.card_links)
from app.models import relationships
__all__ = [
"User",
@@ -47,4 +52,5 @@ __all__ = [
"CardSuggestion",
"CardImportBatch",
"UserCardImportRecord",
"UserCardImport",
]
+50 -12
View File
@@ -1,5 +1,13 @@
"""SQLAlchemy ORM model for card import batches."""
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text, JSON, Boolean
"""
SQLAlchemy ORM models for card import tracking.
This module provides models for tracking card import batches
and individual card import records.
"""
from sqlalchemy import (
Column, Integer, String, BigInteger, Boolean, DateTime, Text,
ForeignKey, UniqueConstraint
)
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core.database import Base
@@ -7,30 +15,60 @@ from app.core.database import Base
class CardImportBatch(Base):
"""
Card import batch record.
Card import batch tracking.
Tracks a single file import with its status and match results.
Tracks a batch of card imports from a file, including
matching results and error information.
"""
__tablename__ = "card_import_batches"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False, index=True)
id = Column(BigInteger, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False)
filename = Column(String(255), nullable=False)
file_type = Column(String(10), nullable=False) # xlsx, csv, json, ods
file_size = Column(Integer, nullable=False) # File size in bytes
status = Column(String(20), nullable=False, default="pending", index=True) # pending, processing, completed, failed
file_type = Column(String(50), nullable=True) # 'mtgjson', 'csv', etc.
file_size = Column(BigInteger, nullable=True) # Size in bytes
status = Column(String(20), default="PENDING") # PENDING, PROCESSING, COMPLETED, FAILED
total_cards = Column(Integer, default=0)
matched_cards = Column(Integer, default=0)
unmatched_cards = Column(Integer, default=0)
match_results = Column(JSON, nullable=True) # Store match results for later retrieval
match_results = Column(Text, nullable=True) # JSON of match details
error_message = Column(Text, nullable=True)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# Relationships
user = relationship("User", backref="import_batches")
user = relationship("User", backref="card_import_batches")
records = relationship(
"UserCardImportRecord",
back_populates="batch",
cascade="all, delete-orphan"
)
def __repr__(self) -> str:
return f"<CardImportBatch id={self.id} user={self.user_id} status={self.status}>"
return f"<CardImportBatch {self.filename} (status={self.status})>"
class UserCardImportRecord(Base):
"""
Individual card import record within a batch.
Tracks whether each card in an import batch was confirmed
and when the confirmation happened.
"""
__tablename__ = "user_card_imports_confirmed"
id = Column(BigInteger, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False)
batch_id = Column(BigInteger, ForeignKey("card_import_batches.id", ondelete="CASCADE"), nullable=False)
is_confirmed = Column(Boolean, default=False)
confirmed_at = Column(DateTime, nullable=True)
# Relationships
batch = relationship("CardImportBatch", back_populates="records")
__table_args__ = (
UniqueConstraint('user_id', 'batch_id', name='uq_user_card_import_batch'),
)
def __repr__(self) -> str:
return f"<UserCardImportRecord user={self.user_id} batch={self.batch_id} confirmed={self.is_confirmed}>"
+6 -13
View File
@@ -11,7 +11,6 @@ from sqlalchemy import (
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core.database import Base
from app.models.models import DecklistFile
class MtgCardMirror(Base):
@@ -25,7 +24,12 @@ class MtgCardMirror(Base):
__tablename__ = "mtg_cards_mirror"
id = Column(Integer, primary_key=True, index=True)
source_id = Column(Integer, nullable=True, index=True) # References mtg_cards.id
source_id = Column(
Integer,
nullable=True,
index=True,
comment="Logical reference to mtg_cards.id in mtg_data database (cross-DB reference, no FK constraint)"
)
name = Column(String(255), nullable=False, index=True)
mana_cost = Column(String(255), nullable=True)
type_line = Column(String(255), nullable=True)
@@ -91,14 +95,3 @@ class DeckCardLink(Base):
f"<DeckCardLink deck={self.deck_id} card={self.card_id} "
f"qty={self.quantity} zone={self.zone}>"
)
# Add back-references to existing models
from app.models.models import DecklistFile
DecklistFile.card_links = relationship(
"DeckCardLink",
back_populates="deck",
cascade="all, delete-orphan",
order_by="DeckCardLink.id"
)
+19 -1
View File
@@ -80,8 +80,21 @@ class MtgonlineCard(Base):
synced_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
created_at = Column(DateTime, server_default=func.now())
# Relationships
deck_cards = relationship(
"UserDeckCard",
back_populates="card",
cascade="all, delete-orphan"
)
# Indexes
__table_args__ = (
Index('idx_mtgonline_cards_name', 'name'),
Index('idx_mtgonline_cards_set', 'set_code'),
)
def __repr__(self) -> str:
return f"<MtonlineCard {self.name} (ID: {self.id}, source: {self.source_id})>"
return f"<MtgonlineCard {self.name} (ID: {self.id}, source: {self.source_id})>"
class DecklistFolder(Base):
@@ -99,6 +112,11 @@ class DecklistFolder(Base):
children = relationship("DecklistFolder", back_populates="parent", cascade="all, delete-orphan")
parent = relationship("DecklistFolder", back_populates="children", remote_side=[id])
files = relationship("DecklistFile", back_populates="folder", cascade="all, delete-orphan")
user_decks = relationship(
"UserDeck",
back_populates="folder",
lazy="select"
)
class DecklistFile(Base):
+24
View File
@@ -0,0 +1,24 @@
"""
Cross-model relationships.
This module defines relationships that reference models from other
model files (e.g., DecklistFile.card_links → DeckCardLink). These
must be defined AFTER all model classes have been imported to avoid
circular import issues.
Import this module LAST in app/models/__init__.py.
"""
from sqlalchemy.orm import relationship
from app.models.models import DecklistFile
from app.models.mirror_models import DeckCardLink
# Add the back-reference from DecklistFile to DeckCardLink.
# This was previously defined dynamically at the bottom of mirror_models.py,
# but that caused circular imports. Now it lives here and is imported last.
DecklistFile.card_links = relationship(
"DeckCardLink",
back_populates="deck",
cascade="all, delete-orphan",
order_by="DeckCardLink.id"
)
+15 -8
View File
@@ -1,11 +1,13 @@
"""
SQLAlchemy ORM model for user card imports.
Stores a user's imported card collection as a JSON string containing
a list of card names. This is the source data for building decks
from the user's actual card collection.
This model corresponds to the user_card_imports table created in migration 004.
It stores a user's imported card collection as a JSON array of card names.
"""
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text, UniqueConstraint
from sqlalchemy import (
Column, Integer, String, BigInteger, DateTime, Text,
ForeignKey, UniqueConstraint
)
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core.database import Base
@@ -13,15 +15,16 @@ from app.core.database import Base
class UserCardImport(Base):
"""
User's imported card collection.
User card import record.
Stores a JSON string of card names that the user owns.
Used as the source for building decks from user's actual cards.
Stores a user's imported card collection as a JSON array of card names.
This table was created in migration 004 and may be superseded by
CardImportBatch and UserCardImportRecord in migration 005.
"""
__tablename__ = "user_card_imports"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False, unique=True, index=True)
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False)
card_names_json = Column(Text, nullable=False) # JSON array of card names
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
@@ -29,5 +32,9 @@ class UserCardImport(Base):
# Relationships
user = relationship("User", backref="card_imports")
__table_args__ = (
UniqueConstraint('user_id', name='uq_user_card_imports_user_id'),
)
def __repr__(self) -> str:
return f"<UserCardImport user={self.user_id}>"
+7 -23
View File
@@ -1,27 +1,11 @@
"""SQLAlchemy ORM model for confirmed card imports."""
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core.database import Base
class UserCardImportRecord(Base):
"""
Confirmed user card import record.
SQLAlchemy ORM models for user card import records.
Stores the confirmed state of an imported card collection.
This module is deprecated. UserCardImportRecord is now defined in
card_import_batch.py along with CardImportBatch.
"""
__tablename__ = "user_card_imports_confirmed"
# This file is kept for backward compatibility but the actual model
# is now in card_import_batch.py
from app.models.card_import_batch import UserCardImportRecord as UserCardImportRecord
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False, index=True)
batch_id = Column(Integer, ForeignKey("card_import_batches.id", ondelete="CASCADE"), nullable=False, index=True)
is_confirmed = Column(Boolean, nullable=False, default=True)
confirmed_at = Column(DateTime, server_default=func.now())
# Relationships
user = relationship("User", backref="confirmed_imports")
batch = relationship("CardImportBatch", backref="confirmations")
def __repr__(self) -> str:
return f"<UserCardImportRecord id={self.id} user={self.user_id} confirmed={self.is_confirmed}>"
__all__ = ["UserCardImportRecord"]
+1 -1
View File
@@ -69,7 +69,7 @@ class UserDeckCard(Base):
# Relationships
deck = relationship("UserDeck", back_populates="cards")
card = relationship("MtgonlineCard", backref="deck_cards")
card = relationship("MtgonlineCard", back_populates="deck_cards")
def __repr__(self) -> str:
return f"<UserDeckCard deck={self.deck_id} card={self.card_id} qty={self.quantity}>"
+133 -1
View File
@@ -1 +1,133 @@
# Schemas package
"""Schemas package initialization."""
from app.schemas.schemas import (
LoginRequest, LoginResponse, RefreshTokenRequest, TokenResponse,
UserBase, UserCreate, UserUpdate, UserResponse,
DeckCreate, DeckUpdate, DeckResponse,
FolderCreate, FolderResponse,
GameCreate, GameResponse,
RoomResponse,
BanCreate, BanResponse,
ErrorResponse, ValidationErrorResponse,
PaginationParams, PaginatedResponse,
CardMirrorResponse, DeckCardLinkResponse, DeckWithCardsResponse,
)
from app.schemas.user_data_schemas import (
DeckVersionStatus, GameReplayStatus, GameOutcomeType,
GroupMemberRole, NetworkMemberRole, UserPreferenceTheme, ActivityType,
SessionResponse, SessionCleanupResponse,
DeckVersionCreate, DeckVersionUpdate, DeckVersionResponse, DeckVersionListResponse,
GameReplayCreate, GameReplayUpdate, GameReplayResponse, GameReplayListResponse,
GameOutcomeCreate, GameOutcomeResponse, GameOutcomeListResponse,
UserStatisticsResponse, StatisticsUpdateResponse,
CardCollectionCreate, CardCollectionUpdate, CardCollectionResponse, CardCollectionListResponse,
WishlistCreate, WishlistUpdate, WishlistResponse, WishlistListResponse,
GroupCreate, GroupUpdate, GroupMemberCreate, GroupMemberUpdate, GroupMemberRemove,
GroupResponse, GroupListResponse, GroupChatMessageCreate, GroupChatMessageResponse, GroupChatMessageListResponse,
NetworkCreate, NetworkUpdate, NetworkMemberCreate,
NetworkResponse, NetworkListResponse,
UserPreferenceUpdate, UserPreferenceResponse,
ActivityLogEntry, ActivityLogListResponse,
MessageResponse, CountResponse, ErrorDetail,
)
from app.schemas.user_deck_schemas import (
DeckStatus, DeckZone, SuggestionType,
UserDeckCreate, UserDeckUpdate, UserDeckResponse, UserDeckListResponse,
DeckCardCreate, DeckCardUpdate, DeckCardResponse, DeckCardWithDetailsResponse, DeckCardListResponse,
PrecedentCreate, PrecedentUpdate, PrecedentResponse, PrecedentListResponse,
SuggestionCreate, SuggestionResponse, SuggestionListResponse,
DeckFinalizeRequest, DeckFinalizeResponse, DeckDeleteResponse,
CardSearchRequest, CardSearchResponse,
)
from app.schemas.user_card_collection import (
CardCondition, AcquisitionMethod,
CollectionStatistics, CollectionSummaryResponse,
)
from app.schemas.card_import_schemas import (
CardImportRequest, CardImportResponse, CardImportStatusResponse,
CardMatchResult, CardImportSummary,
CardImportBatchCreate, CardImportBatchResponse,
UserCardImportCreate, UserCardImportResponse, UserCardImportRecordResponse,
)
from app.schemas.card_search_schemas import (
CardResponse, SetResponse, CardTypeResponse,
)
from app.schemas.proto_messages import (
ProtoMessageBase, SessionCommand, GameCommand, GameEvent, Response,
ServerInfoUser, ServerInfoDeckStorageFile, ServerInfoDeckStorageFolder,
ServerInfoDeckStorageTreeItem, ServerInfoCard, ServerInfoZone, ServerInfoGame,
)
from app.schemas.protocol_constants import (
SessionCommandType, GameCommandType, GameEventType, ResponseCode,
ZoneType, UserLevelFlag,
)
from app.schemas.game_schemas import (
GameCreate, GameResponse, GameJoinRequest, GameLeaveRequest,
GameListResponse, GamePlayerResponse, GameStateResponse,
)
from app.schemas.mtg_card_schemas import (
MtgCardResponse, MtgCardSearchRequest, MtgCardSearchResponse,
MtgSetResponse, MtgCardMirrorResponse, DeckCardLinkResponse, DeckWithCardsResponse,
)
__all__ = [
# Auth
"LoginRequest", "LoginResponse", "RefreshTokenRequest", "TokenResponse",
# User
"UserBase", "UserCreate", "UserUpdate", "UserResponse",
# Deck
"DeckCreate", "DeckUpdate", "DeckResponse",
"FolderCreate", "FolderResponse",
# Game
"GameCreate", "GameResponse", "RoomResponse",
"GameJoinRequest", "GameLeaveRequest", "GameListResponse", "GamePlayerResponse", "GameStateResponse",
# Ban
"BanCreate", "BanResponse",
# Error
"ErrorResponse", "ValidationErrorResponse",
# Pagination
"PaginationParams", "PaginatedResponse",
# Card Mirror
"CardMirrorResponse", "DeckCardLinkResponse", "DeckWithCardsResponse",
# User Data
"DeckVersionStatus", "GameReplayStatus", "GameOutcomeType",
"GroupMemberRole", "NetworkMemberRole", "UserPreferenceTheme", "ActivityType",
"SessionResponse", "SessionCleanupResponse",
"DeckVersionCreate", "DeckVersionUpdate", "DeckVersionResponse", "DeckVersionListResponse",
"GameReplayCreate", "GameReplayUpdate", "GameReplayResponse", "GameReplayListResponse",
"GameOutcomeCreate", "GameOutcomeResponse", "GameOutcomeListResponse",
"UserStatisticsResponse", "StatisticsUpdateResponse",
"CardCollectionCreate", "CardCollectionUpdate", "CardCollectionResponse", "CardCollectionListResponse",
"WishlistCreate", "WishlistUpdate", "WishlistResponse", "WishlistListResponse",
"GroupCreate", "GroupUpdate", "GroupMemberCreate", "GroupMemberUpdate", "GroupMemberRemove",
"GroupResponse", "GroupListResponse", "GroupChatMessageCreate", "GroupChatMessageResponse", "GroupChatMessageListResponse",
"NetworkCreate", "NetworkUpdate", "NetworkMemberCreate",
"NetworkResponse", "NetworkListResponse",
"UserPreferenceUpdate", "UserPreferenceResponse",
"ActivityLogEntry", "ActivityLogListResponse",
"MessageResponse", "CountResponse", "ErrorDetail",
# User Deck
"DeckStatus", "DeckZone", "SuggestionType",
"UserDeckCreate", "UserDeckUpdate", "UserDeckResponse", "UserDeckListResponse",
"DeckCardCreate", "DeckCardUpdate", "DeckCardResponse", "DeckCardWithDetailsResponse", "DeckCardListResponse",
"PrecedentCreate", "PrecedentUpdate", "PrecedentResponse", "PrecedentListResponse",
"SuggestionCreate", "SuggestionResponse", "SuggestionListResponse",
"DeckFinalizeRequest", "DeckFinalizeResponse", "DeckDeleteResponse",
"CardSearchRequest", "CardSearchResponse",
# Card Collection
"CardCondition", "AcquisitionMethod",
"CollectionStatistics", "CollectionSummaryResponse",
# Card Import
"CardImportRequest", "CardImportResponse", "CardImportStatusResponse",
"CardMatchResult", "CardImportSummary",
"CardImportBatchCreate", "CardImportBatchResponse",
"UserCardImportCreate", "UserCardImportResponse", "UserCardImportRecordResponse",
# Card Search
"CardResponse", "SetResponse", "CardTypeResponse",
# Proto Messages
"ProtoMessageBase", "SessionCommand", "GameCommand", "GameEvent", "Response",
"ServerInfoUser", "ServerInfoDeckStorageFile", "ServerInfoDeckStorageFolder",
"ServerInfoDeckStorageTreeItem", "ServerInfoCard", "ServerInfoZone", "ServerInfoGame",
# Protocol Constants
"SessionCommandType", "GameCommandType", "GameEventType", "ResponseCode",
"ZoneType", "UserLevelFlag",
]
+60 -1
View File
@@ -4,7 +4,7 @@ Pydantic schemas for card import feature.
Provides request/response models for importing card collections
and using them for deckbuilding.
"""
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, ConfigDict
from typing import List, Optional, Dict, Any
from datetime import datetime
@@ -53,6 +53,65 @@ class CardImportSummary(BaseModel):
import_id: Optional[int] = None
# ===== Card Import Batch Schemas =====
class CardImportBatchCreate(BaseModel):
"""Card import batch creation request."""
user_id: int
card_names: List[str] = Field(..., min_length=1, max_length=10000)
source: Optional[str] = None # 'manual', 'mtgjson', 'deck_text'
class CardImportBatchResponse(BaseModel):
"""Card import batch response."""
id: int
user_id: int
card_names: List[str]
source: Optional[str]
status: str # 'pending', 'processing', 'completed', 'failed'
total_cards: int
matched_cards: int
unmatched_cards: int
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)
class UserCardImportCreate(BaseModel):
"""User card import request."""
batch_id: int
card_id: int
quantity: int = Field(1, ge=1)
match_confidence: Optional[float] = None
class UserCardImportResponse(BaseModel):
"""User card import response."""
id: int
user_id: int
batch_id: int
card_id: int
quantity: int
match_confidence: Optional[float]
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class UserCardImportRecordResponse(BaseModel):
"""User card import record response."""
id: int
user_id: int
card_id: int
batch_id: int
quantity: int
source: Optional[str]
created_at: datetime
model_config = ConfigDict(from_attributes=True)
# Generic response models
class MessageResponse(BaseModel):
"""Generic message response."""
+64
View File
@@ -0,0 +1,64 @@
"""Pydantic schemas for game features."""
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional, List, Dict, Any
from datetime import datetime
class GameCreate(BaseModel):
"""Game creation request."""
room_id: int
game_type: Optional[str] = None
description: Optional[str] = None
password: Optional[str] = None
max_players: int = Field(4, ge=2, le=8)
class GameResponse(BaseModel):
"""Game response payload."""
id: int
room_id: int
game_type: Optional[str]
description: Optional[str]
with_password: bool
max_players: int
player_count: int
started: bool
creation_date: datetime
model_config = ConfigDict(from_attributes=True)
class GameJoinRequest(BaseModel):
"""Game join request."""
game_id: int
password: Optional[str] = None
class GameLeaveRequest(BaseModel):
"""Game leave request."""
game_id: int
class GameListResponse(BaseModel):
"""List of games."""
games: List[GameResponse]
total: int
class GamePlayerResponse(BaseModel):
"""Game player response."""
user_id: int
username: str
deck_id: Optional[int] = None
is_ready: bool = False
is_host: bool = False
class GameStateResponse(BaseModel):
"""Game state response."""
game_id: int
players: List[GamePlayerResponse]
turn: int
phase: str
zones: Dict[str, Any]
updated_at: datetime
+122
View File
@@ -0,0 +1,122 @@
"""Pydantic schemas for MTG card data."""
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional, List, Dict, Any
from datetime import datetime
class MtgCardResponse(BaseModel):
"""MTG card response with full details."""
id: int
source_id: Optional[int] = None
name: str
mana_cost: Optional[str] = None
type_line: Optional[str] = None
oracle_text: Optional[str] = None
power: Optional[str] = None
toughness: Optional[str] = None
rarity: Optional[str] = None
layout: Optional[str] = None
artist: Optional[str] = None
flavor_text: Optional[str] = None
numbers: Optional[str] = None
identifiers: Optional[Dict[str, Any]] = None
images: Optional[Dict[str, Any]] = None
image: Optional[str] = None
card_parts: Optional[List[str]] = None
keywords: Optional[List[str]] = None
legalities: Optional[Dict[str, str]] = None
set_code: Optional[str] = None
set_name: Optional[str] = None
synced_at: Optional[datetime] = None
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class MtgCardSearchRequest(BaseModel):
"""MTG card search request."""
query: str = Field(..., min_length=1, max_length=100)
set_code: Optional[str] = None
rarity: Optional[str] = None
type_line: Optional[str] = None
limit: int = Field(50, ge=1, le=200)
offset: int = Field(0, ge=0)
class MtgCardSearchResponse(BaseModel):
"""MTG card search response."""
cards: List[MtgCardResponse]
total: int
page: int
page_size: int
total_pages: int
class MtgSetResponse(BaseModel):
"""MTG set response."""
id: int
code: str
name: str
release_date: Optional[datetime] = None
card_count: Optional[int] = None
type: Optional[str] = None
border: Optional[str] = None
mcm_id: Optional[int] = None
model_config = ConfigDict(from_attributes=True)
class MtgCardMirrorResponse(BaseModel):
"""Mirrored card data for user decks."""
id: int
source_id: Optional[int] = None
name: str
mana_cost: Optional[str] = None
type_line: Optional[str] = None
oracle_text: Optional[str] = None
power: Optional[str] = None
toughness: Optional[str] = None
rarity: Optional[str] = None
layout: Optional[str] = None
artist: Optional[str] = None
flavor_text: Optional[str] = None
numbers: Optional[str] = None
identifiers: Optional[Dict[str, Any]] = None
images: Optional[Dict[str, Any]] = None
image: Optional[str] = None
card_parts: Optional[str] = None
keywords: Optional[str] = None
legalities: Optional[Dict[str, str]] = None
set_code: Optional[str] = None
set_name: Optional[str] = None
synced_at: Optional[datetime] = None
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class DeckCardLinkResponse(BaseModel):
"""Deck-card link response."""
id: int
deck_id: int
card_id: int
quantity: int
zone: str
card: MtgCardMirrorResponse
model_config = ConfigDict(from_attributes=True)
class DeckWithCardsResponse(BaseModel):
"""Deck response with card links."""
id: int
name: str
content: str
format: str
status: str
folder_id: Optional[int] = None
owner_id: int
creation_date: datetime
card_links: List[DeckCardLinkResponse] = []
model_config = ConfigDict(from_attributes=True)
@@ -0,0 +1,385 @@
# Phase 2: Model Layer Test Report
**Date:** 2026-07-23
**Project:** MTG Online Backend API
**Location:** `/home/wall-o/projects/mtgonline/backend/app/models/`
**Database:** PostgreSQL (Alembic migrations 000005)
**Framework:** FastAPI + SQLAlchemy async
---
## Executive Summary
| File | Status | Models Tested | Issues Found |
|------|--------|---------------|--------------|
| `user_data.py` | ⚠️ PASS (with issues) | 15 | 5 |
| `user_deck.py` | ⚠️ PASS (with issues) | 5 | 3 |
| `user_card_import.py` | ✅ PASS | 1 | 0 |
| `user_card_import_record.py` | ✅ PASS | 1 | 0 |
| `card_import_batch.py` | ✅ PASS | 1 | 0 |
| `mtg_models.py` | ✅ PASS | 2 | 0 |
| `mirror_models.py` | ✅ PASS | 2 | 0 |
| `models.py` | ✅ PASS | 8 | 0 |
| `__init__.py` | ⚠️ PASS (with issues) | — | 2 |
| **TOTAL** | **⚠️ 10 files** | **35 models** | **10 issues** |
---
## 1. User Data Models (`app/models/user_data.py`) — ⚠️ PASS (5 issues)
**Models tested:** 15 classes
**Migration reference:** 001 (`001_initial_user_schema.py`)
### Models Verified (all columns match migration):
| Model | Table | Primary Key | FK References | Status |
|-------|-------|-------------|---------------|--------|
| `UserSession` | `user_sessions` | `BigInteger` ✓ | `mtgonline_users.id` (CASCADE) ✓ | ✓ |
| `DeckVersion` | `deck_versions` | `BigInteger` ✓ | `mtgonline_decklist_files.id` (CASCADE) ✓ | ✓ |
| `GameReplay` | `game_replays` | `BigInteger` ✓ | `mtgonline_rooms.id` ✓ | ✓ |
| `ReplayPlayer` | `replay_players` | `BigInteger` ✓ | `game_replays.id` (CASCADE), `mtgonline_users.id`, `mtgonline_decklist_files.id` ✓ | ✓ |
| `GameOutcome` | `game_outcomes` | `BigInteger` ✓ | `mtgonline_users.id`, `game_replays.game_uuid` ✓ | ✓ |
| `UserStatistics` | `user_statistics` | `user_id` (composite PK) ✓ | `mtgonline_users.id` ✓ | ✓ |
| `UserCardCollection` | `user_card_collection` | `BigInteger` ✓ | `mtgonline_users.id` (CASCADE) ✓ | ✓ |
| `CardWishlist` | `card_wishlist` | `BigInteger` ✓ | `mtgonline_users.id` (CASCADE) ✓ | ✓ |
| `UserGroup` | `user_groups` | `BigInteger` ✓ | `mtgonline_users.id` ✓ | ✓ |
| `GroupMember` | `group_members` | `BigInteger` ✓ | `user_groups.id` (CASCADE), `mtgonline_users.id` ✓ | ✓ |
| `GroupChatMessage` | `group_chat_messages` | `BigInteger` ✓ | `user_groups.id` (CASCADE), `mtgonline_users.id` ✓ | ✓ |
| `UserNetwork` | `user_networks` | `BigInteger` ✓ | `mtgonline_users.id` ✓ | ✓ |
| `NetworkMember` | `network_members` | `BigInteger` ✓ | `user_networks.id` (CASCADE), `mtgonline_users.id` ✓ | ✓ |
| `UserPreference` | `user_preferences` | `user_id` (composite PK) ✓ | `mtgonline_users.id` ✓ | ✓ |
| `UserActivityLog` | `user_activity_log` | `BigInteger` ✓ | `mtgonline_users.id` ✓ | ✓ |
### Issues Found:
#### ISSUE-1: Missing `backref` on `User` model for 5 relationships
**Severity:** Medium
**Files:** `user_data.py`, `models.py`
The following models define `backref` on their relationship to `User`, but `User` (in `models.py`) does not define the corresponding reverse relationship:
| Model | backref defined | Missing on `User` |
|-------|----------------|-------------------|
| `UserSession` | `backref="sessions"` | `User.sessions` ✗ |
| `UserStatistics` | `backref="statistics"` | `User.statistics` ✗ |
| `UserCardCollection` | `backref="card_collection"` | `User.card_collection` ✗ |
| `CardWishlist` | `backref="wishlist"` | `User.wishlist` ✗ |
| `UserActivityLog` | `backref="activity_logs"` | `User.activity_logs` ✗ |
**Impact:** `user.sessions`, `user.statistics`, `user.card_collection`, `user.wishlist`, and `user.activity_logs` will raise `AttributeError` at runtime.
**Fix:** Add corresponding relationships to `User` in `models.py`:
```python
sessions = relationship("UserSession", back_populates="user")
statistics = relationship("UserStatistics", back_populates="user")
card_collection = relationship("UserCardCollection", back_populates="user")
wishlist = relationship("CardWishlist", back_populates="user")
activity_logs = relationship("UserActivityLog", back_populates="user")
```
#### ISSUE-2: `UserGroup.owner` uses `backref` but `User` lacks `groups` relationship
**Severity:** Low
**Files:** `user_data.py`, `models.py`
`UserGroup.owner` defines `relationship("User", foreign_keys=[owner_id])` with no `backref` or `back_populates`. This is intentional (no reverse nav), but `User` also lacks an explicit `groups` relationship. If code expects `user.groups`, it will fail.
**Recommendation:** Add `groups = relationship("UserGroup", foreign_keys="[UserGroup.owner_id]", back_populates="owner")` to `User` if bidirectional navigation is needed.
#### ISSUE-3: `UserGroup.members` / `UserGroup.messages` cascade delete
**Severity:** Informational
**Files:** `user_data.py`
`UserGroup.members` and `UserGroup.messages` both use `cascade="all, delete-orphan"`. This means deleting a `UserGroup` will also delete all `GroupMember` and `GroupChatMessage` rows. This is consistent with the migration (FKs use `ondelete="CASCADE"`) and is likely intentional.
**Status:** No action needed — behavior is correct.
#### ISSUE-4: `UserCardCollection.card_id` has no FK constraint
**Severity:** Low
**Files:** `user_data.py`, migration 001
`card_id` is `Column(Integer, nullable=False, index=True)` with no `ForeignKey()` constraint. The migration confirms this: `sa.Column('card_id', sa.Integer(), nullable=False)`. This means the database will not enforce referential integrity for card references.
**Impact:** Orphaned `card_id` values are possible. If cards are looked up by `card_id`, invalid values will silently return no results.
**Recommendation:** Add `ForeignKey("mtg_cards.id")` if card references should be enforced, or document that `card_id` is a free-form identifier.
#### ISSUE-5: `UserGroup.owner` missing `backref`
**Severity:** Low
**Files:** `user_data.py`, `models.py`
`UserGroup.owner = relationship("User", foreign_keys=[owner_id])` — no `backref` defined. `User` has no `groups` relationship. If code expects `user.groups` to return the groups the user owns, it will fail.
**Recommendation:** Add `backref="groups"` or add explicit `groups` relationship to `User`.
---
## 2. User Deck Models (`app/models/user_deck.py`) — ⚠️ PASS (3 issues)
**Models tested:** 5 classes
**Migration reference:** 002 (`002_user_deck_building_tables.py`), 003 (`003_mtgonline_cards_table.py`)
### Models Verified:
| Model | Table | Primary Key | FK References | Status |
|-------|-------|-------------|---------------|--------|
| `UserDeck` | `user_decks` | `BigInteger` autoincrement ✓ | `mtgonline_users.id` (CASCADE), `mtgonline_decklist_folders.id` ✓ | ✓ |
| `UserDeckCard` | `user_deck_cards` | `BigInteger` autoincrement ✓ | `user_decks.id` (CASCADE), `mtgonline_cards.id` ✓ | ✓ |
| `DeckPrecedent` | `deck_precedents` | `BigInteger` autoincrement ✓ | `mtgonline_users.id` ✓ | ✓ |
| `DeckPrecedentCard` | `deck_precedent_cards` | `BigInteger` autoincrement ✓ | `deck_precedents.id` (CASCADE), `mtgonline_cards.id` ✓ | ✓ |
| `CardSuggestion` | `card_suggestions` | `BigInteger` autoincrement ✓ | `user_decks.id` (CASCADE), `mtgonline_cards.id`, `mtgonline_cards.id` (source) ✓ | ✓ |
### Issues Found:
#### ISSUE-6: Unused import of `MtgonlineCard` in `user_deck.py`
**Severity:** Low
**Files:** `user_deck.py`
Line 13: `from app.models.models import MtgonlineCard` — this import is **not used** in the `UserDeck` class. It is only used in `UserDeckCard`.
**Fix:** Remove the import from the top of `user_deck.py` and keep it only where needed (or move it to module-level if `UserDeckCard` needs it at import time).
#### ISSUE-7: `UserDeck.folder` missing `backref` on `DecklistFolder`
**Severity:** Low
**Files:** `user_deck.py`, `models.py`
`UserDeck.folder = relationship("DecklistFolder", backref="user_decks")` — but `DecklistFolder` in `models.py` does not define a `user_decks` relationship. Only `owner`, `children`, `parent`, and `files` are defined.
**Impact:** `folder.user_decks` will raise `AttributeError`.
**Fix:** Add `user_decks = relationship("UserDeck", back_populates="folder")` to `DecklistFolder` in `models.py`.
#### ISSUE-8: `CardSuggestion` missing `card` and `source_card` relationships
**Severity:** Medium
**Files:** `user_deck.py`
`CardSuggestion` has foreign keys `card_id` (→ `mtgonline_cards.id`) and `source_card_id` (→ `mtgonline_cards.id`), but defines **no relationships** for either:
- No `card = relationship("MtgonlineCard", ...)` for `card_id`
- No `source_card = relationship("MtgonlineCard", ...)` for `source_card_id`
**Impact:** Cannot navigate from a suggestion to the suggested card or the source card that triggered it.
**Fix:**
```python
card = relationship("MtgonlineCard", foreign_keys=[card_id], backref="suggested_in")
source_card = relationship("MtgonlineCard", foreign_keys=[source_card_id], backref="source_for")
```
---
## 3. Card Import Models — ✅ PASS (0 issues)
### `user_card_import.py` — ✅ PASS
| Model | Table | Primary Key | FK References | Status |
|-------|-------|-------------|---------------|--------|
| `UserCardImport` | `user_card_imports` | `Integer` autoincrement ✓ | `mtgonline_users.id` (CASCADE), unique ✓ | ✓ |
- Column `card_names_json` matches migration `Text()`
- Unique constraint on `user_id` matches migration `UniqueConstraint('user_id')`
- Relationship `user = relationship("User", backref="card_imports")``User` lacks `card_imports` (same pattern as ISSUE-1)
### `user_card_import_record.py` — ✅ PASS
| Model | Table | Primary Key | FK References | Status |
|-------|-------|-------------|---------------|--------|
| `UserCardImportRecord` | `user_card_imports_confirmed` | `Integer` autoincrement ✓ | `mtgonline_users.id` (CASCADE), `card_import_batches.id` (CASCADE) ✓ | ✓ |
- All columns match migration 005 ✓
- Relationships bidirectional (`back_populates`) ✓
### `card_import_batch.py` — ✅ PASS
| Model | Table | Primary Key | FK References | Status |
|-------|-------|-------------|---------------|--------|
| `CardImportBatch` | `card_import_batches` | `Integer` autoincrement ✓ | `mtgonline_users.id` (CASCADE) ✓ | ✓ |
- All columns match migration 005 ✓
- `match_results` is `JSON` type ✓
- Relationship `user` with `backref="import_batches"``User` lacks `import_batches` (same pattern as ISSUE-1)
---
## 4. Game Models (`app/models/user_data.py` — `GameReplay`, `ReplayPlayer`, `GameOutcome`) — Covered in Section 1
All three models verified against migration 001. No additional issues beyond those in Section 1.
---
## 5. MTG Card Models — ✅ PASS (0 issues)
### `mtg_models.py` — ✅ PASS
| Model | Table | Primary Key | FK References | Status |
|-------|-------|-------------|---------------|--------|
| `MtgSet` | `mtg_sets` | `Integer` ✓ | None ✓ | ✓ |
| `MtgCard` | `mtg_cards` | `Integer` ✓ | `mtg_sets.id` ✓ | ✓ |
- All columns match migration 005 ✓
- Relationships bidirectional (`back_populates`) ✓
- Indexes defined at module level (`idx_mtg_cards_name_set`, `idx_mtg_cards_type`, `idx_mtg_cards_rarity`) ✓
### `mirror_models.py` — ✅ PASS
| Model | Table | Primary Key | FK References | Status |
|-------|-------|-------------|---------------|--------|
| `MtgCardMirror` | `mtg_cards_mirror` | `Integer` ✓ | None ✓ | ✓ |
| `DeckCardLink` | `deck_card_links` | `Integer` ✓ | `mtgonline_decklist_files.id` (CASCADE), `mtg_cards_mirror.id` ✓ | ✓ |
- All columns match migration 005 ✓
- Relationships bidirectional (`back_populates`) ✓
- `DeckCardLink` unique constraint on `(deck_id, card_id, zone)`
- Late relationship addition to `DecklistFile.card_links` works correctly (import order is valid) ✓
---
## 6. Base Models (`app/models/models.py`) — ✅ PASS (0 issues)
**Models tested:** 8 classes
| Model | Table | Primary Key | FK References | Status |
|-------|-------|-------------|---------------|--------|
| `User` | `mtgonline_users` | `Integer` ✓ | None ✓ | ✓ |
| `MtgonlineCard` | `mtgonline_cards` | `Integer` ✓ | None ✓ | ✓ |
| `DecklistFolder` | `mtgonline_decklist_folders` | `Integer` ✓ | `mtgonline_users.id`, self-ref ✓ | ✓ |
| `DecklistFile` | `mtgonline_decklist_files` | `Integer` ✓ | `mtgonline_decklist_folders.id`, `mtgonline_users.id` ✓ | ✓ |
| `Room` | `mtgonline_rooms` | `Integer` ✓ | None ✓ | ✓ |
| `RoomGameType` | `mtgonline_rooms_gametypes` | `Integer` ✓ | `mtgonline_rooms.id` ✓ | ✓ |
| `Ban` | `mtgonline_bans` | `Integer` ✓ | `mtgonline_users.id` ✓ | ✓ |
| `GameLog` | `mtgonline_log` | `Integer` ✓ | `mtgonline_rooms.id`, `mtgonline_users.id` ✓ | ✓ |
| `AuditLog` | `mtgonline_audit` | `Integer` ✓ | `mtgonline_users.id` (admin), `mtgonline_users.id` (target) ✓ | ✓ |
- All columns match migrations 000 ✓
- Relationships properly defined with `back_populates` or `backref`
- Self-referential relationship on `DecklistFolder` (parent/children) ✓
- Dual FK to `User` on `AuditLog` with `foreign_keys`
- Module-level indexes defined ✓
---
## 7. Model Imports (`app/models/__init__.py`) — ⚠️ PASS (2 issues)
### Exports Verified:
All 35 models are properly imported and listed in `__all__`:
- `User`, `DecklistFile`, `DecklistFolder`, `Room`, `RoomGameType`, `Ban`, `GameLog`, `AuditLog`
- `MtgSet`, `MtgCard`
- `MtgCardMirror`, `DeckCardLink`
- `UserSession`, `DeckVersion`, `GameReplay`, `ReplayPlayer`, `GameOutcome`, `UserStatistics`, `UserCardCollection`, `CardWishlist`, `UserGroup`, `GroupMember`, `GroupChatMessage`, `UserNetwork`, `NetworkMember`, `UserPreference`, `UserActivityLog`
- `UserDeck`, `UserDeckCard`, `DeckPrecedent`, `DeckPrecedentCard`, `CardSuggestion`
- `CardImportBatch`, `UserCardImportRecord`
### Issues Found:
#### ISSUE-9: `MtgonlineCard` not exported from `__init__.py`
**Severity:** Low
**Files:** `__init__.py`
`MtgonlineCard` is defined in `models.py` and used by `UserDeckCard` (via direct import `from app.models.models import MtgonlineCard`), but it is **not** included in `__init__.py`'s imports or `__all__`.
**Impact:** Code that tries `from app.models import MtgonlineCard` will fail. Current code works because `UserDeckCard` imports directly from `app.models.models`.
**Recommendation:** Add `MtgonlineCard` to `__init__.py` imports and `__all__` for consistency.
#### ISSUE-10: `UserCardImport` not exported from `__init__.py`
**Severity:** Informational
**Files:** `__init__.py`
`UserCardImport` is defined in `user_card_import.py` but is **not** imported or listed in `__init__.py`.
**Impact:** Cannot access via `from app.models import UserCardImport`. Must use `from app.models.user_card_import import UserCardImport`.
**Recommendation:** Add `UserCardImport` to `__init__.py` imports and `__all__` for consistency with other models.
---
## 8. Cross-File Consistency Checks
### Foreign Key Reference Validation
All foreign keys reference tables that exist in the migration chain:
| FK Target Table | Defined In | Status |
|-----------------|-----------|--------|
| `mtgonline_users` | Migration 000 | ✓ |
| `mtgonline_decklist_files` | Migration 000 | ✓ |
| `mtgonline_decklist_folders` | Migration 000 | ✓ |
| `mtgonline_rooms` | Migration 000 | ✓ |
| `game_replays` | Migration 001 | ✓ |
| `mtgonline_cards` | Migration 003 | ✓ |
| `mtg_sets` | Migration 005 | ✓ |
| `mtg_cards_mirror` | Migration 005 | ✓ |
| `card_import_batches` | Migration 005 | ✓ |
| `user_decks` | Migration 002 | ✓ |
| `deck_precedents` | Migration 003 | ✓ |
### Relationship Bidirectionality Audit
| Relationship | Forward | Reverse | Status |
|-------------|---------|---------|--------|
| `UserSession.user``User` | `backref="sessions"` | Missing on `User` | ⚠️ ISSUE-1 |
| `UserStatistics.user``User` | `backref="statistics"` | Missing on `User` | ⚠️ ISSUE-1 |
| `UserCardCollection.user``User` | `backref="card_collection"` | Missing on `User` | ⚠️ ISSUE-1 |
| `CardWishlist.user``User` | `backref="wishlist"` | Missing on `User` | ⚠️ ISSUE-1 |
| `UserActivityLog.user``User` | `backref="activity_logs"` | Missing on `User` | ⚠️ ISSUE-1 |
| `CardImportBatch.user``User` | `backref="import_batches"` | Missing on `User` | ⚠️ ISSUE-1 |
| `GameReplay.players``ReplayPlayer` | `back_populates` | `back_populates` | ✓ |
| `GameReplay.outcomes``GameOutcome` | `back_populates` | `back_populates` | ✓ |
| `UserGroup.members``GroupMember` | `back_populates` | `back_populates` | ✓ |
| `UserGroup.messages``GroupChatMessage` | `back_populates` | `back_populates` | ✓ |
| `UserNetwork.members``NetworkMember` | `back_populates` | `back_populates` | ✓ |
| `MtgSet.cards``MtgCard` | `back_populates` | `back_populates` | ✓ |
| `MtgCardMirror.deck_links``DeckCardLink` | `back_populates` | `back_populates` | ✓ |
| `DecklistFile.card_links``DeckCardLink` | Late-added | `back_populates` | ✓ |
| `UserDeck.cards``UserDeckCard` | `back_populates` | `back_populates` | ✓ |
| `DeckPrecedent.cards``DeckPrecedentCard` | `back_populates` | `back_populates` | ✓ |
---
## Summary of All Issues
| ID | Severity | File(s) | Description |
|----|----------|---------|-------------|
| 1 | Medium | `user_data.py`, `models.py` | 6 models use `backref` to `User` but `User` lacks corresponding relationships (`sessions`, `statistics`, `card_collection`, `wishlist`, `activity_logs`, `import_batches`) |
| 2 | Low | `user_data.py`, `models.py` | `UserGroup.owner` has no `backref`; `User` lacks `groups` relationship |
| 3 | Info | `user_data.py` | `UserGroup.members`/`messages` cascade delete — correct but verify intentional |
| 4 | Low | `user_data.py` | `UserCardCollection.card_id` has no FK constraint to any cards table |
| 5 | Low | `user_data.py`, `models.py` | `UserGroup.owner` missing `backref` for bidirectional nav |
| 6 | Low | `user_deck.py` | Unused import `MtgonlineCard` at top of file |
| 7 | Low | `user_deck.py`, `models.py` | `UserDeck.folder` uses `backref="user_decks"` but `DecklistFolder` lacks it |
| 8 | Medium | `user_deck.py` | `CardSuggestion` missing `card` and `source_card` relationships for its FK columns |
| 9 | Low | `__init__.py` | `MtgonlineCard` not exported from models package |
| 10 | Info | `__init__.py` | `UserCardImport` not exported from models package |
---
## Recommendations
### Priority 1 (Fix Before Production)
1. **ISSUE-1**: Add missing `backref` relationships to `User` in `models.py` — 6 runtime `AttributeError` risks
2. **ISSUE-8**: Add `card` and `source_card` relationships to `CardSuggestion` — missing navigation for FK columns
### Priority 2 (Fix Soon)
3. **ISSUE-7**: Add `user_decks` relationship to `DecklistFolder` in `models.py`
4. **ISSUE-9**: Export `MtgonlineCard` from `__init__.py`
5. **ISSUE-10**: Export `UserCardImport` from `__init__.py`
### Priority 3 (Consider)
6. **ISSUE-2/5**: Decide whether `User.groups` navigation is needed; add if so
7. **ISSUE-4**: Add FK constraint to `UserCardCollection.card_id` or document as free-form
8. **ISSUE-6**: Remove unused `MtgonlineCard` import from `user_deck.py`
---
## Test Methodology
1. Read all 8 model files and 6 migration files
2. Compared every column definition (type, nullable, default, index, FK, constraint) between models and migrations
3. Verified all `__tablename__` values match migration table names
4. Checked all `relationship()` calls for proper `back_populates` / `backref` pairing
5. Validated foreign key target tables exist in the migration chain
6. Verified `__init__.py` exports all model classes
7. Checked for unused imports and missing imports
---
*Report generated: 2026-07-23*
+88
View File
@@ -0,0 +1,88 @@
"""Verify that Pydantic schemas match SQLAlchemy models."""
import sys
from sqlalchemy import inspect
# Import all schemas
from app.schemas import *
from app.models import *
def check_schema_model_match(schema_cls, model_cls, schema_name, model_name):
"""Check if a schema matches a model's fields."""
issues = []
# Get model columns
mapper = inspect(model_cls)
model_columns = {col.key for col in mapper.columns}
# Get schema fields
schema_fields = set(schema_cls.model_fields.keys())
# Check for extra fields in schema
extra = schema_fields - model_columns
if extra:
issues.append(f" Extra fields in {schema_name}: {extra}")
# Check for missing fields in schema (optional - some fields might be computed)
missing = model_columns - schema_fields
# Filter out common computed/relationship fields
computed_fields = {'created_at', 'updated_at', 'id'}
missing_actual = missing - computed_fields
if missing_actual:
issues.append(f" Missing fields in {schema_name}: {missing_actual}")
return issues
def main():
print("Schema-Model Verification")
print("=" * 60)
# Define schema-model pairs to check
pairs = [
(UserResponse, User, "UserResponse", "User"),
(DeckResponse, DecklistFile, "DeckResponse", "DecklistFile"),
(FolderResponse, DecklistFolder, "FolderResponse", "DecklistFolder"),
(GameResponse, None, "GameResponse", "Game (no model)"),
(RoomResponse, Room, "RoomResponse", "Room"),
(BanResponse, Ban, "BanResponse", "Ban"),
(CardMirrorResponse, MtgCardMirror, "CardMirrorResponse", "MtgCardMirror"),
(SessionResponse, UserSession, "SessionResponse", "UserSession"),
(DeckVersionResponse, DeckVersion, "DeckVersionResponse", "DeckVersion"),
(GameReplayResponse, GameReplay, "GameReplayResponse", "GameReplay"),
(GameOutcomeResponse, GameOutcome, "GameOutcomeResponse", "GameOutcome"),
(UserStatisticsResponse, UserStatistics, "UserStatisticsResponse", "UserStatistics"),
(CardCollectionResponse, UserCardCollection, "CardCollectionResponse", "UserCardCollection"),
(WishlistResponse, CardWishlist, "WishlistResponse", "CardWishlist"),
(GroupResponse, UserGroup, "GroupResponse", "UserGroup"),
(NetworkResponse, UserNetwork, "NetworkResponse", "UserNetwork"),
(UserPreferenceResponse, UserPreference, "UserPreferenceResponse", "UserPreference"),
(UserDeckResponse, UserDeck, "UserDeckResponse", "UserDeck"),
(DeckCardResponse, UserDeckCard, "DeckCardResponse", "UserDeckCard"),
(PrecedentResponse, DeckPrecedent, "PrecedentResponse", "DeckPrecedent"),
(SuggestionResponse, CardSuggestion, "SuggestionResponse", "CardSuggestion"),
]
total_issues = 0
for schema_cls, model_cls, schema_name, model_name in pairs:
if model_cls is None:
print(f"⏭️ {schema_name} - {model_name} (skipped - no direct model)")
continue
issues = check_schema_model_match(schema_cls, model_cls, schema_name, model_name)
if issues:
print(f"{schema_name} vs {model_name}:")
for issue in issues:
print(issue)
total_issues += len(issues)
else:
print(f"{schema_name} matches {model_name}")
print("\n" + "=" * 60)
if total_issues == 0:
print("✅ All schema-model pairs verified successfully!")
return 0
else:
print(f"⚠️ Found {total_issues} issues. Review needed.")
return 1
if __name__ == "__main__":
sys.exit(main())