> ## Documentation Index
> Fetch the complete documentation index at: https://docs.freezeray.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Migration Testing

> How FreezeRay validates migration paths

# Migration Testing

Migration testing validates that your schema migrations work correctly by loading real frozen databases and attempting to migrate them. This catches crashes and data corruption **before** production.

## The Problem

### Fresh Installs vs. Migrations

When you test your app in the simulator, you're almost always testing **fresh installs**:

```swift theme={null}
// Simulator test (fresh install):
// 1. No existing database
// 2. SwiftData creates NEW database with current schema
// 3. No migration happens
// ✅ Test passes

// Production user (existing database):
// 1. Existing database with v1.0.0 schema
// 2. SwiftData must MIGRATE v1.0.0 → v2.0.0
// 3. Migration fails or corrupts data
// ❌ App crashes on launch
```

**The result:** Your tests pass, but real users crash.

### Migration Crashes Are Silent Killers

Production crashes from broken migrations:

```
Cannot use staged migration with an unknown model version
```

This error means you modified a shipped schema. Fresh installs don't catch this because they skip migration entirely.

### Data Corruption Is Worse Than Crashes

Sometimes migrations "succeed" but silently corrupt data:

* Non-optional properties added without defaults → undefined behavior
* Transformable properties with changed types → old data becomes nil
* Properties removed → user data silently dropped
* Type conversions fail → data loss

Users don't know their data is corrupted until it's too late.

## The Solution: Load Real Databases

FreezeRay tests migrations using **actual frozen databases** from shipped versions:

```swift theme={null}
@Test("Migrate v1.0.0 → v2.0.0")
func testMigrateV1toV2() throws {
    // Loads the REAL v1.0.0 database from FreezeRay/Fixtures/1.0.0/
    // Attempts migration using your SchemaMigrationPlan
    // Crashes in tests if migration is broken
    try FreezeRayRuntime.testMigration(
        from: AppSchemaV1.self,
        to: AppSchemaV2.self,
        migrationPlan: AppMigrations.self
    )
}
```

If your migration is broken, you get the crash **in your test suite** where you can debug it.

## How It Works

### 1. Frozen Fixtures as Test Data

When you freeze a schema, FreezeRay captures the SQLite database:

```
FreezeRay/Fixtures/
├── 1.0.0/
│   ├── App-1_0_0.sqlite   ← Real v1.0.0 database
│   └── ...
└── 2.0.0/
    ├── App-2_0_0.sqlite   ← Real v2.0.0 database
    └── ...
```

These fixtures are committed to your repo and included in your test bundle.

### 2. Generate Migration Tests

When you start working on v2.0.0, generate the migration test:

```bash theme={null}
freezeray generate migration-tests --from-schema 1.0.0 --to-schema 2.0.0
```

This creates a migration test template:

```swift theme={null}
// AUTO-GENERATED by FreezeRay CLI
// This file is scaffolded once and owned by you. Customize as needed.

@Suite(.serialized)
struct MigrateV1_0_0toV2_0_0_Tests {
    @Test("Migrate v1.0.0 → v2.0.0")
    func testMigrateV1_0_0toV2_0_0() throws {
        // Test the migration using FreezeRayRuntime
        try FreezeRayRuntime.testMigration(
            from: AppSchemaV1.self,
            to: AppSchemaV2.self,
            migrationPlan: AppMigrations.self
        )

        // TODO: Add data integrity checks here
        // Example:
        // - Verify data is preserved during migration
        // - Check that new fields have default values
        // - Validate relationship updates
        // - Ensure no data loss for critical fields
    }
}
```

### 3. Runtime Migration Testing

`FreezeRayRuntime.testMigration()` does the following:

1. **Copies** the frozen v1.0.0 database to a temporary location
2. **Creates** a ModelContainer with your `SchemaMigrationPlan`
3. **Attempts migration** from v1.0.0 → v2.0.0
4. **Throws** if migration fails or crashes

If migration succeeds, control returns to your test where you can add custom validation.

## Test Types

### Generated Migration Tests

Created when you start working on a new schema version:

```bash theme={null}
# When starting v2.0.0 development
freezeray generate migration-tests --from-schema 1.0.0 --to-schema 2.0.0
# Creates: FreezeRay/Tests/MigrateV1_0_0toV2_0_0_Tests.swift
```

**What it tests:**

* Migration doesn't crash
* SwiftData can open the migrated database
* Basic migration mechanics work

**What it doesn't test (you add this):**

* Data integrity validation
* Custom migration logic correctness
* Performance

### Custom Data Validation

You should add validation to check data integrity:

```swift theme={null}
@Test("Migrate v1.0.0 → v2.0.0")
func testMigrateV1_0_0toV2_0_0() throws {
    try FreezeRayRuntime.testMigration(
        from: AppSchemaV1.self,
        to: AppSchemaV2.self,
        migrationPlan: AppMigrations.self
    )

    // ✅ Add custom validation
    let container = try ModelContainer(
        for: AppSchemaV2.self,
        configurations: ModelConfiguration(/* migrated db */)
    )

    let context = container.mainContext
    let users = try context.fetch(FetchDescriptor<DataV2.User>())

    // Verify record counts
    #expect(users.count == 10, "Expected 10 users after migration")

    // Check required fields preserved
    for user in users {
        #expect(!user.name.isEmpty, "User name should not be empty")
        #expect(!user.email.isEmpty, "User email should not be empty")
    }

    // Validate new fields have defaults
    for user in users {
        #expect(user.createdAt != nil, "New createdAt field should have default")
    }
}
```

## Multi-Hop Migrations

SwiftData runs migrations sequentially: V1 → V2 → V3

FreezeRay tests each hop:

```swift theme={null}
// Test direct migrations
@Test("Migrate v1.0.0 → v2.0.0")
func testMigrateV1toV2() throws { /* ... */ }

@Test("Migrate v2.0.0 → v3.0.0")
func testMigrateV2toV3() throws { /* ... */ }

// Optional: Test multi-hop migration
@Test("Migrate v1.0.0 → v3.0.0 (multi-hop)")
func testMigrateV1toV3() throws {
    // Tests the full migration path: V1 → V2 → V3
    try FreezeRayRuntime.testMigration(
        from: AppSchemaV1.self,
        to: AppSchemaV3.self,
        migrationPlan: AppMigrations.self
    )
}
```

**Why test multi-hop?**

* Users might skip app versions (1.0 user updates directly to 3.0)
* Cumulative migrations can expose bugs not visible in single hops
* Data transformations across multiple hops can cause unexpected issues

## Common Migration Issues

### Issue 1: Modified Shipped Schema

```swift theme={null}
// ❌ You modified AppSchemaV1 after shipping
enum AppSchemaV1: VersionedSchema {
    static var models: [any PersistentModel.Type] {
        [User.self, Post.self]  // Added Post after shipping!
    }
}
```

**Error:**

```
Cannot use staged migration with an unknown model version
```

**Fix:**
Don't modify shipped schemas. Create AppSchemaV2 instead.

### Issue 2: Missing Models

```swift theme={null}
// ❌ Forgot to include User in V2
enum AppSchemaV2: VersionedSchema {
    static var models: [any PersistentModel.Type] {
        [Post.self]  // Where's User?
    }
}
```

**Error:**

```
Persistent store migration failed, missing source managed object model
```

**Fix:**
Include ALL models in each schema version.

### Issue 3: Data Loss in Custom Migrations

```swift theme={null}
// ❌ Custom migration doesn't preserve data
static let migrateV1toV2 = MigrationStage.custom(
    fromVersion: AppSchemaV1.self,
    toVersion: AppSchemaV2.self,
    willMigrate: nil,
    didMigrate: { context in
        // Deletes all users!
        try context.delete(model: DataV1.User.self)
    }
)
```

**No error** - migration succeeds, but data is gone!

**Fix:**
Add test validation to catch data loss:

```swift theme={null}
@Test func testMigrateV1toV2() throws {
    try FreezeRayRuntime.testMigration(/* ... */)

    // ✅ Verify data preserved
    let users = try context.fetch(FetchDescriptor<DataV2.User>())
    #expect(users.count > 0, "Users should be preserved")
}
```

## Best Practices

<Warning>
  **Always customize scaffolded tests.** The TODO markers are reminders to add data validation.
</Warning>

### Add Meaningful Assertions

```swift theme={null}
// ❌ Bad: No validation
@Test func testMigration() throws {
    try FreezeRayRuntime.testMigration(/* ... */)
}

// ✅ Good: Validates data integrity
@Test func testMigration() throws {
    try FreezeRayRuntime.testMigration(/* ... */)

    // Check record counts
    #expect(users.count == expectedCount)

    // Validate required fields
    for user in users {
        #expect(!user.email.isEmpty)
    }

    // Check relationships intact
    for post in posts {
        #expect(post.author != nil)
    }
}
```

### Test Edge Cases

```swift theme={null}
@Test func testMigrationWithEmptyDatabase() throws {
    // What if database has no records?
}

@Test func testMigrationWithMaxRecords() throws {
    // What if database has thousands of records?
}

@Test func testMigrationWithCorruptedData() throws {
    // What if transformable data is malformed?
}
```

### Run Tests in CI

Migration tests should run on every PR:

```yaml theme={null}
# .github/workflows/test.yml
- name: Run Migration Tests
  run: xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 15'
```

## Troubleshooting

### "Fixture not found" error

**Cause:** Fixtures not included in test bundle

**Fix:**

1. Add `FreezeRay/` folder to test target resources
2. Ensure folder reference is "folder" (blue) not "group" (yellow)

### Migration test passes locally, fails in CI

**Cause:** Fixtures not committed to git

**Fix:**

```bash theme={null}
git add FreezeRay/Fixtures/
git commit -m "Add migration fixtures"
```

### Test crashes with "cannot open database"

**Cause:** Database file permissions in test bundle

**Fix:**
Copy fixture to writable location before testing (FreezeRayRuntime does this automatically)

## Next Steps

<Columns cols={2}>
  <Card title="Testing Migrations Guide" icon="flask" href="/guides/testing-migrations">
    Step-by-step tutorial for custom validation
  </Card>

  <Card title="Drift Detection" icon="triangle-exclamation" href="/concepts/drift-detection">
    Catch schema changes before testing migrations
  </Card>

  <Card title="Schema Freezing" icon="snowflake" href="/concepts/schema-freezing">
    Learn how to create frozen fixtures
  </Card>

  <Card title="CI Integration" icon="github" href="/guides/ci-integration">
    Run migration tests in your CI pipeline
  </Card>
</Columns>
