# Database Migration Audit Report **Project:** mtgonline backend **Date:** 2026-07-23 **Migrations Reviewed:** 5 files in `alembic/versions/` --- ## Migration Chain | # | File | Revision | Down Revision | Tables Created | |---|------|----------|---------------|----------------| | 0 | `000_base_tables.py` | `000` | `None` | `mtgonline_users`, `mtgonline_decklist_files`, `mtgonline_rooms` | | 1 | `001_initial_user_schema.py` | `001` | `000` | 15 user data tables + re-creates 3 base tables | | 2 | `002_user_deck_building_tables.py` | `002` | `001` | `user_decks`, `user_deck_cards`, `deck_precedents`, `deck_precedent_cards`, `card_suggestions` | | 3 | `003_mtgonline_cards_table.py` | `003` | `002` | `mtgonline_cards` | | 4 | `004_card_import_table.py` | `004` | `003` | `user_card_imports` | --- ## Migration-by-Migration Status ### Migration 000: `000_base_tables.py` — ⚠️ PASS (with warnings) **Status:** PASS **Issues:** None critical. | Check | Result | |-------|--------| | `upgrade()` exists | ✅ | | `downgrade()` exists | ✅ | | `mtgonline_users` columns correct | ✅ 20 columns, proper PK, indexes, unique constraints | | `mtgonline_decklist_files` columns correct | ✅ 11 columns, FK to users, indexes | | `mtgonline_rooms` columns correct | ✅ 12 columns, FK to users, indexes | | Foreign keys valid | ✅ All reference `mtgonline_users.id` | | Indexes created | ✅ `idx_decklist_files_user`, `idx_decklist_files_name`, `idx_rooms_created_by`, `idx_rooms_name` | | Unique constraints | ✅ `username`, `email` on users | | Downgrade order correct | ✅ Drops dependent tables first | --- ### Migration 001: `001_initial_user_schema.py` — ❌ FAIL (Critical) **Status:** FAIL **Critical Issue:** Re-creates base tables that migration 000 already created. #### Critical Issues | # | Issue | Severity | Details | |---|-------|----------|---------| | 1 | **Duplicate table creation** | 🔴 CRITICAL | The `upgrade()` function re-creates `mtgonline_users`, `mtgonline_decklist_files`, and `mtgonline_rooms` — tables that migration 000 already created. This will cause `sqlalchemy.exc.ProgrammingError: relation "mtgonline_users" already exists` when running `alembic upgrade head`. | | 2 | **Missing `mtgonline_decklist_folders` table** | 🔴 CRITICAL | `user_decks.folder_id` (migration 002) references `mtgonline_decklist_folders.id`, but this table is **never created in any migration**. It only exists in the ORM model (`models.py`). Migration 002 will fail with a FK error. | | 3 | **Missing `mtgonline_rooms_gametypes` table** | 🟡 WARNING | `RoomGameType` model references `mtgonline_rooms_gametypes` table, never created in any migration. | | 4 | **Missing `mtgonline_bans` table** | 🟡 WARNING | `Ban` model references `mtgonline_bans` table, never created in any migration. | | 5 | **Missing `mtgonline_log` table** | 🟡 WARNING | `GameLog` model references `mtgonline_log` table, never created in any migration. | | 6 | **Missing `mtgonline_audit` table** | 🟡 WARNING | `AuditLog` model references `mtgonline_audit` table, never created in any migration. | #### Table Creation Analysis (001 upgrade) All 15 dependent tables are created correctly with valid foreign keys: | Table | FK References | Valid? | |-------|--------------|--------| | `user_sessions` | `mtgonline_users.id` | ✅ | | `deck_versions` | `mtgonline_decklist_files.id` | ✅ (if base tables exist) | | `game_replays` | `mtgonline_rooms.id` | ✅ (if base tables exist) | | `replay_players` | `game_replays.id`, `mtgonline_users.id`, `mtgonline_decklist_files.id` | ✅ | | `game_outcomes` | `mtgonline_users.id`, `game_replays.game_uuid` | ✅ | | `user_statistics` | `mtgonline_users.id` (PK) | ✅ | | `user_card_collection` | `mtgonline_users.id` | ✅ | | `card_wishlist` | `mtgonline_users.id` | ✅ | | `user_groups` | `mtgonline_users.id` | ✅ | | `group_members` | `user_groups.id`, `mtgonline_users.id` | ✅ | | `group_chat_messages` | `user_groups.id`, `mtgonline_users.id` | ✅ | | `user_networks` | `mtgonline_users.id` | ✅ | | `network_members` | `user_networks.id`, `mtgonline_users.id` | ✅ | | `user_preferences` | `mtgonline_users.id` (PK) | ✅ | | `user_activity_log` | `mtgonline_users.id` | ✅ | #### Downgrade Analysis | Check | Result | |-------|--------| | Drop order correct | ✅ (reverse dependency order) | | Indexes dropped | ⚠️ Not explicitly dropped (but `op.drop_table()` handles this) | | Base tables dropped | ✅ (at end, after dependents) | #### Comment Numbering Issue The `upgrade()` function has inconsistent section numbering: - "0. Base Tables" → "0.1. Rooms Table" → "1. User Sessions" → "2. Deck Versions" → **"4. Game Replays"** (skips 3) --- ### Migration 002: `002_user_deck_building_tables.py` — ❌ FAIL (Critical) **Status:** FAIL **Critical Issue:** References non-existent `mtgonline_decklist_folders` table. | Check | Result | |-------|--------| | `upgrade()` exists | ✅ | | `downgrade()` exists | ✅ | | `user_decks` FK to `mtgonline_decklist_folders.id` | ❌ **Table never created in any migration** | | `user_deck_cards` FK to `user_decks.id` | ✅ | | `deck_precedent_cards` FK to `deck_precedents.id` | ✅ | | `card_suggestions` FK to `user_decks.id` | ✅ | | Unique constraints | ✅ `uq_deck_card_unique`, `uq_precedent_card_unique`, `uq_suggestion_unique` | | Indexes created | ✅ | | Downgrade order correct | ✅ | #### Additional Issues | # | Issue | Severity | |---|-------|----------| | 7 | `user_deck_cards.card_id` has no FK constraint in migration, but model defines `ForeignKey("mtgonline_cards.id")` | 🟡 WARNING | | 8 | `card_suggestions.source_card_id` has no FK constraint in migration | 🟡 WARNING | | 9 | `deck_precedent_cards.card_id` has no FK constraint in migration | 🟡 WARNING | | 10 | `deck_precedents.created_by` has no FK constraint in migration | 🟡 WARNING | --- ### Migration 003: `003_mtgonline_cards_table.py` — ✅ PASS **Status:** PASS | Check | Result | |-------|--------| | `upgrade()` exists | ✅ | | `downgrade()` exists | ✅ | | `mtgonline_cards` columns correct | ✅ 22 columns | | Indexes created | ✅ `idx_mtgonline_cards_name`, `idx_mtgonline_cards_set` | | Foreign keys | None (standalone table) | | Unique constraints | None | --- ### Migration 004: `004_card_import_table.py` — ⚠️ PASS (with warnings) **Status:** PASS **Issues:** Minor consistency issues. | Check | Result | |-------|--------| | `upgrade()` exists | ✅ | | `downgrade()` exists | ✅ | | `user_card_imports` columns correct | ✅ 5 columns | | FK to `mtgonline_users.id` | ✅ | | Unique constraint `uq_user_card_imports_user_id` | ✅ | | Index `idx_user_card_imports_user` | ✅ | | Primary key definition | ⚠️ `sa.Column('id', sa.Integer(), autoincrement=True, nullable=False)` + `sa.PrimaryKeyConstraint('id')` — redundant but functional | --- ## Cross-Reference: Models vs Migrations ### Tables in Models but NOT in Any Migration (🔴 CRITICAL) | Model | Table Name | Referenced By | |-------|-----------|---------------| | `DecklistFolder` | `mtgonline_decklist_folders` | `DecklistFile.folder_id`, `UserDeck.folder_id` | | `RoomGameType` | `mtgonline_rooms_gametypes` | `Room.game_types` | | `Ban` | `mtgonline_bans` | Admin ban records | | `GameLog` | `mtgonline_log` | Game chat logs | | `AuditLog` | `mtgonline_audit` | Admin action audit trail | | `MtgCardMirror` | `mtg_cards_mirror` | Card mirror for deckbuilding | | `DeckCardLink` | `deck_card_links` | Junction: decks ↔ mirrored cards | | `CardImportBatch` | `card_import_batches` | Card import batch tracking | | `UserCardImportRecord` | `user_card_imports_confirmed` | Confirmed import records | **These 9 tables must be created in migrations before any migration that references them can succeed.** ### Column Type Mismatches (Model vs Migration) #### `mtgonline_users` | Column | Migration | Model | Match? | |--------|-----------|-------|--------| | `username` | `String(50)` | `String(64)` | ❌ | | `password_hash` | `String(255)` | `String(128)` | ❌ | | `salt` | `String(32)` | `String(128)` | ❌ | | `display_name` | `String(100)` | **Not in model** | ⚠️ | | `avatar_url` | `String(500)` | **Not in model** | ⚠️ | | `country` | `String(100)` | `String(2)` | ❌ | | `real_name` | `String(255)` | `String(128)` | ❌ | | `avatar_bmp` | `LargeBinary()` | `Text` | ❌ | | `privlevel` | `Integer()` | `String(50)` | ❌ | | `is_active` | `Boolean()` | `Boolean()` | ✅ | | `is_banned` | `Boolean()` | `Boolean()` | ✅ | | `ban_reason` | `Text()` | `Text()` | ✅ | | `ban_ends` | `DateTime()` | `DateTime()` | ✅ | | `vip_status` | `Boolean()` | `Integer()` | ❌ | | `vip_expiry` | `DateTime()` | `DateTime()` | ✅ | | `creation_date` | `DateTime()` | `DateTime()` | ✅ | | `last_login` | `DateTime()` | `DateTime()` | ✅ | #### `mtgonline_decklist_files` | Column | Migration | Model | Match? | |--------|-----------|-------|--------| | `user_id` | FK column | `owner_id` | ❌ (different name) | | `name` | `String(255)` | `String(255)` | ✅ | | `content` | `Text()` | `Text()` (nullable=True) | ⚠️ | | `description` | `Text()` | **Not in model** | ⚠️ | | `format` | `String(50), default='standard'` | `String(50), default='native'` | ❌ | | `is_favorite` | `Boolean()` | **Not in model** | ⚠️ | | `import_source` | `String(50)` | **Not in model** | ⚠️ | | `import_confidence` | `Float()` | **Not in model** | ⚠️ | | `last_played` | `DateTime()` | **Not in model** | ⚠️ | | **Missing** | — | `folder_id` | ❌ | | **Missing** | — | `status` | ❌ | #### `mtgonline_rooms` | Column | Migration | Model | Match? | |--------|-----------|-------|--------| | `name` | `String(100)` | `String(100), unique=True` | ⚠️ (migration missing unique) | | `description` | `Text()` | `Text()` | ✅ | | `max_players` | `Integer(), default=8` | **Not in model** | ⚠️ | | `is_public` | `Boolean()` | **Not in model** | ⚠️ | | `is_password_protected` | `Boolean()` | `Boolean()` | ✅ | | `password_hash` | `String(255)` | `String(128)` | ❌ | | `game_type` | `String(50)` | **Not in model** | ⚠️ | | `format` | `String(50)` | **Not in model** | ⚠️ | | `created_by` | `Integer()` | **Not in model** | ⚠️ | --- ## Summary of All Issues ### 🔴 Critical (Must Fix Before Deployment) | # | Issue | Location | Impact | |---|-------|----------|--------| | 1 | **Migration 001 re-creates base tables** | `001_initial_user_schema.py:upgrade()` | `alembic upgrade head` will FAIL — tables already exist from migration 000 | | 2 | **`mtgonline_decklist_folders` never created** | Missing from all migrations | Migration 002 (`user_decks.folder_id`) will FAIL with FK error | | 3 | **9 model tables have no migration** | `mtgonline_decklist_folders`, `mtgonline_rooms_gametypes`, `mtgonline_bans`, `mtgonline_log`, `mtgonline_audit`, `mtg_cards_mirror`, `deck_card_links`, `card_import_batches`, `user_card_imports_confirmed` | Any code referencing these tables will fail at runtime | ### 🟡 Warnings (Should Fix) | # | Issue | Location | Impact | |---|-------|----------|--------| | 4 | Column type mismatches (15+ columns) | Migration 000/001 vs models | Schema drift — DB won't match ORM definitions | | 5 | Column name mismatch (`user_id` vs `owner_id`) | `mtgonline_decklist_files` | ORM won't map correctly | | 6 | Missing `unique=True` on `mtgonline_rooms.name` | Migration 000 | Model defines it as unique | | 7 | Missing FK constraints on junction table columns | Migration 002 | `card_id`, `source_card_id`, `created_by` lack FK references | | 8 | Inconsistent section numbering in migration 001 | `001_initial_user_schema.py` | Code readability | | 9 | Redundant PK definition in migration 004 | `004_card_import_table.py` | Works but messy | ### ℹ️ Informational | # | Issue | Location | |---|-------|----------| | 10 | Two card table models: `MtgCardMirror` (mtg_cards_mirror) and `MtonlineCard` (mtgonline_cards) | Different tables, different purposes | | 11 | Migration 001 creates `user_card_collection` without composite unique constraint that model defines | Model has `UniqueConstraint('user_id', 'card_id', 'is_foil', 'is_alt_art')` | | 12 | `user_card_collection` migration missing composite index `idx_collection_user_card` | Model defines it | --- ## Recommendations ### Immediate (Blockers) 1. **Remove duplicate table creation from migration 001** — Delete the `mtgonline_users`, `mtgonline_decklist_files`, and `mtgonline_rooms` `op.create_table()` calls from `001_initial_user_schema.py`. These are already created by migration 000. 2. **Create migration for `mtgonline_decklist_folders`** — This table is referenced by both `mtgonline_decklist_files.folder_id` (model) and `user_decks.folder_id` (migration 002). Add it before migration 002 runs. 3. **Create migrations for all missing tables** — At minimum: `mtgonline_rooms_gametypes`, `mtgonline_bans`, `mtgonline_log`, `mtgonline_audit`, `mtg_cards_mirror`, `deck_card_links`, `card_import_batches`, `user_card_imports_confirmed`. ### Short-term (Consistency) 4. **Align migration schemas with ORM models** — Fix all column type mismatches and missing columns. The migrations should be the source of truth for the database, and models should match. 5. **Add missing FK constraints in migration 002** — Add `ForeignKey` to `card_id`, `source_card_id`, and `created_by` columns. 6. **Add missing unique constraint on `mtgonline_rooms.name`** — Migration 000 should include `unique=True`. 7. **Add missing constraints to `user_card_collection`** — Migration 001 should include the composite unique constraint and index that the model defines. ### Long-term (Architecture) 8. **Decide on migration strategy** — Either: - (a) Remove migration 001's duplicate base tables and keep migration 000 as the single source of base table creation, OR - (b) Remove migration 000 entirely and let migration 001 handle all base tables (but this is risky for a production database). 9. **Add Alembic environment script** — Create `alembic/env.py` with `include_object` filter to auto-detect table creation order and prevent circular dependencies. 10. **Consider using `op.create_foreign_key()` explicitly** — Some FK definitions in the migrations use inline `ForeignKey()` which is fine, but explicit `op.create_foreign_key()` calls are more readable and Alembic can better track them for downgrade.