# 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.
