axiom-swiftdata-migration-diag

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SwiftData Migration Diagnostics

SwiftData 迁移诊断

Overview

概述

SwiftData migration failures manifest as production crashes, data loss, corrupted relationships, or simulator-only success. Core principle 90% of migration failures stem from missing models in VersionedSchema, relationship inverse issues, or untested migration paths—not SwiftData bugs.
SwiftData迁移失败表现为生产环境崩溃、数据丢失、关联关系损坏,或仅在模拟器中正常运行。核心原则 90%的迁移失败源于VersionedSchema中缺失模型、关联关系逆属性问题或未测试的迁移路径——而非SwiftData框架本身的bug。

Red Flags — Suspect SwiftData Migration Issue

危险信号——怀疑是SwiftData迁移问题

If you see ANY of these, suspect a migration configuration problem:
  • App crashes on launch after schema change
  • "Expected only Arrays for Relationships" error
  • "The model used to open the store is incompatible with the one used to create the store"
  • "Failed to fulfill faulting for [relationship]"
  • Migration works in simulator but crashes on real device
  • Data exists before migration, gone after
  • Relationships broken after migration (nil where they shouldn't be)
  • FORBIDDEN "SwiftData migrations are broken, we should use Core Data"
    • SwiftData handles millions of migrations in production apps
    • Schema mismatches and relationship errors are always configuration, not framework
    • Do not rationalize away the issue—diagnose it
Critical distinction Simulator deletes the database on each rebuild, hiding schema mismatch issues. Real devices keep persistent databases and crash immediately on schema mismatch. MANDATORY: Test migrations on real device with real data before shipping.
如果出现以下任何一种情况,怀疑是迁移配置问题:
  • 架构变更后应用启动崩溃
  • 出现"Expected only Arrays for Relationships"错误
  • 出现"The model used to open the store is incompatible with the one used to create the store"错误
  • 出现"Failed to fulfill faulting for [relationship]"错误
  • 迁移在模拟器中正常运行但在真机上崩溃
  • 迁移前数据存在,迁移后丢失
  • 迁移后关联关系损坏(不应为nil的字段变为nil)
  • 严禁 认为"SwiftData迁移有问题,我们应该改用Core Data"
    • SwiftData在生产应用中已处理数百万次迁移
    • 架构不匹配和关联关系错误始终是配置问题,而非框架问题
    • 不要回避问题——要诊断问题
关键区别 模拟器会在每次重建时删除数据库,隐藏架构不匹配问题。真机保留持久化数据库,一旦架构不匹配会立即崩溃。强制要求:上线前必须在真机上使用真实数据测试迁移。

Mandatory First Steps

强制第一步操作

ALWAYS run these FIRST (before changing code):
swift
// 1. Identify the crash/issue type
// Screenshot the crash message and note:
//   - "Expected only Arrays" = relationship inverse missing
//   - "incompatible model" = schema version mismatch
//   - "Failed to fulfill faulting" = relationship integrity broken
//   - Simulator works, device crashes = untested migration path
// Record: "Error type: [exact message]"

// 2. Check schema version configuration
// In your migration plan:
enum MigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        // ✅ VERIFY: All versions in order?
        // ✅ VERIFY: Latest version matches container?
        [SchemaV1.self, SchemaV2.self, SchemaV3.self]
    }

    static var stages: [MigrationStage] {
        // ✅ VERIFY: Migration stages match schema transitions?
        [migrateV1toV2, migrateV2toV3]
    }
}

// In your app:
let schema = Schema(versionedSchema: SchemaV3.self)  // ✅ VERIFY: Matches latest in plan?
let container = try ModelContainer(
    for: schema,
    migrationPlan: MigrationPlan.self  // ✅ VERIFY: Plan is registered?
)
// Record: "Schema version: latest is [version]"

// 3. Check all models included in VersionedSchema
enum SchemaV2: VersionedSchema {
    static var models: [any PersistentModel.Type] {
        // ✅ VERIFY: Are ALL models listed? (even unchanged ones)
        [Note.self, Folder.self, Tag.self]
    }
}
// Record: "Missing models? Yes/no"

// 4. Check relationship inverse declarations
@Model
final class Note {
    @Relationship(deleteRule: .nullify, inverse: \Folder.notes)  // ✅ VERIFY: inverse specified?
    var folder: Folder?

    @Relationship(deleteRule: .nullify, inverse: \Tag.notes)  // ✅ VERIFY: inverse specified?
    var tags: [Tag] = []
}
// Record: "Relationship inverses: all specified? Yes/no"

// 5. Enable SwiftData debug logging
// In Xcode scheme, add argument:
// -com.apple.coredata.swiftdata.debug 1
// Run and check Console for SQL queries
// Record: "Debug log shows: [what you see]"
在修改代码前,务必先执行以下步骤
swift
// 1. Identify the crash/issue type
// Screenshot the crash message and note:
//   - "Expected only Arrays" = relationship inverse missing
//   - "incompatible model" = schema version mismatch
//   - "Failed to fulfill faulting" = relationship integrity broken
//   - Simulator works, device crashes = untested migration path
// Record: "Error type: [exact message]"

// 2. Check schema version configuration
// In your migration plan:
enum MigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        // ✅ VERIFY: All versions in order?
        // ✅ VERIFY: Latest version matches container?
        [SchemaV1.self, SchemaV2.self, SchemaV3.self]
    }

    static var stages: [MigrationStage] {
        // ✅ VERIFY: Migration stages match schema transitions?
        [migrateV1toV2, migrateV2toV3]
    }
}

// In your app:
let schema = Schema(versionedSchema: SchemaV3.self)  // ✅ VERIFY: Matches latest in plan?
let container = try ModelContainer(
    for: schema,
    migrationPlan: MigrationPlan.self  // ✅ VERIFY: Plan is registered?
)
// Record: "Schema version: latest is [version]"

// 3. Check all models included in VersionedSchema
enum SchemaV2: VersionedSchema {
    static var models: [any PersistentModel.Type] {
        // ✅ VERIFY: Are ALL models listed? (even unchanged ones)
        [Note.self, Folder.self, Tag.self]
    }
}
// Record: "Missing models? Yes/no"

// 4. Check relationship inverse declarations
@Model
final class Note {
    @Relationship(deleteRule: .nullify, inverse: \Folder.notes)  // ✅ VERIFY: inverse specified?
    var folder: Folder?

    @Relationship(deleteRule: .nullify, inverse: \Tag.notes)  // ✅ VERIFY: inverse specified?
    var tags: [Tag] = []
}
// Record: "Relationship inverses: all specified? Yes/no"

// 5. Enable SwiftData debug logging
// In Xcode scheme, add argument:
// -com.apple.coredata.swiftdata.debug 1
// Run and check Console for SQL queries
// Record: "Debug log shows: [what you see]"

What this tells you

这些步骤能告诉你什么

  • "Expected only Arrays for Relationships" → Proceed to Pattern 1 (relationship inverse fix)
  • "incompatible model" → Proceed to Pattern 2 (schema version mismatch)
  • Missing models in VersionedSchema → Proceed to Pattern 3 (complete schema snapshot)
  • Simulator works, device crashes → Proceed to Pattern 4 (migration testing)
  • Data lost after migration → Proceed to Pattern 5 (willMigrate/didMigrate misuse)
  • 出现"Expected only Arrays for Relationships" → 参考模式1(修复关联关系逆属性)
  • 出现"incompatible model" → 参考模式2(修复架构版本不匹配)
  • VersionedSchema中缺失模型 → 参考模式3(完整架构快照)
  • 模拟器正常,真机崩溃 → 参考模式4(迁移测试)
  • 迁移后数据丢失 → 参考模式5(修复willMigrate/didMigrate误用)

MANDATORY INTERPRETATION

强制解读

Before changing ANY code, identify ONE of these:
  1. If error is "Expected only Arrays" AND relationship inverse missing → Relationship configuration issue
  2. If error mentions "incompatible" AND schema versions don't match → Version mismatch
  3. If models are missing from VersionedSchema → Incomplete schema snapshot
  4. If simulator succeeds but device fails → Untested migration path
  5. If data exists before but not after → willMigrate/didMigrate limitation violated
在修改任何代码前,确定以下问题类型之一:
  1. 错误为"Expected only Arrays"且缺失关联关系逆属性 → 关联关系配置问题
  2. 错误提及"incompatible"且架构版本不匹配 → 版本不匹配问题
  3. VersionedSchema中缺失模型 → 架构快照不完整
  4. 模拟器正常但真机崩溃 → 迁移路径未测试
  5. 迁移前数据存在但迁移后丢失 → 违反了willMigrate/didMigrate的使用限制

If diagnostics are contradictory or unclear

如果诊断结果矛盾或不清晰

  • STOP. Do NOT proceed to patterns yet
  • Add
    -com.apple.coredata.swiftdata.debug 1
    and examine SQL output
  • Check file system: does .sqlite file exist? What size?
  • Establish baseline: what's actually happening vs. what you assumed

  • 停止操作。先不要应用任何修复模式
  • 添加
    -com.apple.coredata.swiftdata.debug 1
    参数并检查SQL输出
  • 检查文件系统:.sqlite文件是否存在?大小是多少?
  • 建立基准:实际发生了什么 vs 你假设的情况

Verifying Migration Completed Successfully

验证迁移是否成功完成

Use this section when migration appears to complete without errors, but you want to verify data integrity.
当迁移看似无错误完成,但你需要验证数据完整性时使用本节内容。

Quick Verification Checklist

快速验证清单

After migration runs without crashing:
swift
// 1. Verify record count matches pre-migration
let context = container.mainContext
let postMigrationCount = try context.fetch(FetchDescriptor<Note>()).count
print("Post-migration count: \(postMigrationCount)")
// Compare to pre-migration count

// 2. Spot-check specific records
let sampleNote = try context.fetch(
    FetchDescriptor<Note>(predicate: #Predicate { $0.id == "known-test-id" })
).first
print("Sample note title: \(sampleNote?.title ?? "MISSING")")

// 3. Verify relationships intact
if let note = sampleNote {
    print("Folder relationship: \(note.folder != nil ? "✓" : "✗")")
    print("Tags count: \(note.tags.count)")

    // Verify inverse relationships
    if let folder = note.folder {
        let folderHasNote = folder.notes.contains { $0.id == note.id }
        print("Inverse relationship: \(folderHasNote ? "✓" : "✗")")
    }
}

// 4. Check for orphaned data
let orphanedNotes = try context.fetch(
    FetchDescriptor<Note>(predicate: #Predicate { $0.folder == nil })
)
print("Orphaned notes (should be 0 if cascade delete worked): \(orphanedNotes.count)")
迁移无崩溃完成后:
swift
// 1. Verify record count matches pre-migration
let context = container.mainContext
let postMigrationCount = try context.fetch(FetchDescriptor<Note>()).count
print("Post-migration count: \(postMigrationCount)")
// Compare to pre-migration count

// 2. Spot-check specific records
let sampleNote = try context.fetch(
    FetchDescriptor<Note>(predicate: #Predicate { $0.id == "known-test-id" })
).first
print("Sample note title: \(sampleNote?.title ?? "MISSING")")

// 3. Verify relationships intact
if let note = sampleNote {
    print("Folder relationship: \(note.folder != nil ? "✓" : "✗")")
    print("Tags count: \(note.tags.count)")

    // Verify inverse relationships
    if let folder = note.folder {
        let folderHasNote = folder.notes.contains { $0.id == note.id }
        print("Inverse relationship: \(folderHasNote ? "✓" : "✗")")
    }
}

// 4. Check for orphaned data
let orphanedNotes = try context.fetch(
    FetchDescriptor<Note>(predicate: #Predicate { $0.folder == nil })
)
print("Orphaned notes (should be 0 if cascade delete worked): \(orphanedNotes.count)")

What Successful Migration Looks Like

成功迁移的表现

Console Output:
Post-migration count: 1523  // Matches pre-migration
Sample note title: Test Note  // Not "MISSING"
Folder relationship: ✓
Tags count: 3
Inverse relationship: ✓
Orphaned notes: 0
If you see:
  • Record count differs → Data loss (check willMigrate logic)
  • "MISSING" records → Schema mismatch or fetch error
  • Relationships nil → Inverse configuration or prefetching issue
  • Orphaned records >0 → Cascade delete rule not working
See patterns below for specific fixes.

控制台输出:
Post-migration count: 1523  // Matches pre-migration
Sample note title: Test Note  // Not "MISSING"
Folder relationship: ✓
Tags count: 3
Inverse relationship: ✓
Orphaned notes: 0
如果看到以下情况:
  • 记录数不一致 → 数据丢失(检查willMigrate逻辑)
  • 出现"MISSING"记录 → 架构不匹配或查询错误
  • 关联关系为nil → 逆属性配置或预取问题
  • 孤立记录数>0 → 级联删除规则未生效
参考以下模式获取具体修复方案。

Decision Tree

决策树

SwiftData migration problem suspected?
├─ Error: "Expected only Arrays for Relationships"?
│  └─ YES → Relationship inverse missing
│     ├─ Many-to-many relationship? → Pattern 1a (explicit inverse)
│     ├─ One-to-many relationship? → Pattern 1b (verify both sides)
│     └─ iOS 17.0 alphabetical bug? → Pattern 1c (default value workaround)
├─ Error: "incompatible model" or crash on launch?
│  └─ YES → Schema version mismatch
│     ├─ Latest schema not in plan? → Pattern 2a (add to schemas array)
│     ├─ Migration stage missing? → Pattern 2b (add stage)
│     └─ Container using wrong schema? → Pattern 2c (verify version)
├─ Migration runs but data missing?
│  └─ YES → Data loss during migration
│     ├─ Used didMigrate to access old models? → Pattern 3a (use willMigrate)
│     ├─ Forgot to save in willMigrate? → Pattern 3b (add context.save())
│     └─ Custom migration logic wrong? → Pattern 3c (debug transformation)
├─ Works in simulator but crashes on device?
│  └─ YES → Untested migration path
│     ├─ Never tested on real device? → Pattern 4a (real device testing)
│     ├─ Never tested upgrade path? → Pattern 4b (test v1 → v2 upgrade)
│     └─ Production data differs from test? → Pattern 4c (test with prod data)
└─ Relationships nil after migration?
   └─ YES → Relationship integrity broken
      ├─ Forgot to prefetch relationships? → Pattern 5a (add prefetching)
      ├─ Inverse relationship wrong? → Pattern 5b (fix inverse)
      └─ Delete rule caused cascade? → Pattern 5c (check delete rules)

SwiftData迁移问题疑似?
├─ 错误:"Expected only Arrays for Relationships"?
│  └─ 是 → 缺失关联关系逆属性
│     ├─ 多对多关联? → 模式1a(显式逆属性)
│     ├─ 一对多关联? → 模式1b(验证双方配置)
│     └─ iOS 17.0字母序bug? → 模式1c(默认值 workaround)
├─ 错误:"incompatible model"或启动崩溃?
│  └─ 是 → 架构版本不匹配
│     ├─ 最新架构未加入迁移计划? → 模式2a(添加到schemas数组)
│     ├─ 缺失迁移阶段? → 模式2b(添加迁移阶段)
│     └─ 容器使用错误架构? → 模式2c(验证版本)
├─ 迁移完成但数据丢失?
│  └─ 是 → 迁移过程中数据丢失
│     ├─ 在didMigrate中访问旧模型? → 模式3a(使用willMigrate)
│     ├─ willMigrate中忘记保存? → 模式3b(添加context.save())
│     └─ 自定义迁移逻辑错误? → 模式3c(调试转换逻辑)
├─ 模拟器正常但真机崩溃?
│  └─ 是 → 迁移路径未测试
│     ├─ 从未在真机上测试? → 模式4a(真机测试)
│     ├─ 从未测试升级路径? → 模式4b(测试v1 → v2升级)
│     └─ 生产数据与测试数据不同? → 模式4c(使用生产数据测试)
└─ 迁移后关联关系为nil?
   └─ 是 → 关联关系完整性损坏
      ├─ 忘记预取关联关系? → 模式5a(添加预取)
      ├─ 逆属性配置错误? → 模式5b(修复逆属性)
      └─ 删除规则导致级联删除? → 模式5c(检查删除规则)

Common Patterns

常见修复模式

Pattern 1a: Fix "Expected only Arrays for Relationships"

模式1a:修复"Expected only Arrays for Relationships"

PRINCIPLE Many-to-many relationships require explicit inverse declarations.
原则 多对多关联关系需要显式声明逆属性。

❌ WRONG (Causes "Expected only Arrays" error)

❌ 错误写法(导致"Expected only Arrays"错误)

swift
@Model
final class Note {
    var tags: [Tag] = []  // ❌ Missing inverse
}

@Model
final class Tag {
    var notes: [Note] = []  // ❌ Missing inverse
}
swift
@Model
final class Note {
    var tags: [Tag] = []  // ❌ Missing inverse
}

@Model
final class Tag {
    var notes: [Note] = []  // ❌ Missing inverse
}

✅ CORRECT (Explicit inverse)

✅ 正确写法(显式逆属性)

swift
@Model
final class Note {
    @Relationship(deleteRule: .nullify, inverse: \Tag.notes)
    var tags: [Tag] = []  // ✅ Inverse specified
}

@Model
final class Tag {
    @Relationship(deleteRule: .nullify, inverse: \Note.tags)
    var notes: [Note] = []  // ✅ Inverse specified
}
Why this works SwiftData requires explicit inverse for many-to-many to create junction table correctly.
Time cost 2 minutes to add inverse declarations

swift
@Model
final class Note {
    @Relationship(deleteRule: .nullify, inverse: \Tag.notes)
    var tags: [Tag] = []  // ✅ Inverse specified
}

@Model
final class Tag {
    @Relationship(deleteRule: .nullify, inverse: \Note.tags)
    var notes: [Note] = []  // ✅ Inverse specified
}
为什么有效 SwiftData需要显式逆属性才能正确创建多对多关联的中间表。
耗时 2分钟添加逆属性声明

Pattern 1b: iOS 17.0 Alphabetical Bug Workaround

模式1b:iOS 17.0字母序Bug的Workaround

PRINCIPLE In iOS 17.0, many-to-many relationships could fail if model names were in alphabetical order.
原则 在iOS 17.0中,如果模型名称按字母序排列,多对多关联可能失败。

❌ WRONG (Crashes in iOS 17.0)

❌ 错误写法(在iOS 17.0中崩溃)

swift
@Model
final class Actor {
    @Relationship(deleteRule: .nullify, inverse: \Movie.actors)
    var movies: [Movie]  // ❌ No default value
}

@Model
final class Movie {
    @Relationship(deleteRule: .nullify, inverse: \Actor.movies)
    var actors: [Actor]  // ❌ No default value
}
// Crashes if "Actor" < "Movie" alphabetically
swift
@Model
final class Actor {
    @Relationship(deleteRule: .nullify, inverse: \Movie.actors)
    var movies: [Movie]  // ❌ No default value
}

@Model
final class Movie {
    @Relationship(deleteRule: .nullify, inverse: \Actor.movies)
    var actors: [Actor]  // ❌ No default value
}
// Crashes if "Actor" < "Movie" alphabetically

✅ CORRECT (Works in iOS 17.0+)

✅ 正确写法(在iOS 17.0+中有效)

swift
@Model
final class Actor {
    @Relationship(deleteRule: .nullify, inverse: \Movie.actors)
    var movies: [Movie] = []  // ✅ Default value
}

@Model
final class Movie {
    @Relationship(deleteRule: .nullify, inverse: \Actor.movies)
    var actors: [Actor] = []  // ✅ Default value
}
Fixed in iOS 17.1+
Time cost 1 minute to add default values

swift
@Model
final class Actor {
    @Relationship(deleteRule: .nullify, inverse: \Movie.actors)
    var movies: [Movie] = []  // ✅ Default value
}

@Model
final class Movie {
    @Relationship(deleteRule: .nullify, inverse: \Actor.movies)
    var actors: [Actor] = []  // ✅ Default value
}
修复版本 iOS 17.1+
耗时 1分钟添加默认值

Pattern 2a: Schema Version Mismatch

模式2a:修复架构版本不匹配

PRINCIPLE Migration plan's schemas array must include ALL versions in order.
原则 迁移计划的schemas数组必须按顺序包含所有版本。

❌ WRONG (Missing version causes crash)

❌ 错误写法(缺失版本导致崩溃)

swift
enum MigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [SchemaV1.self, SchemaV3.self]  // ❌ Missing V2!
    }

    static var stages: [MigrationStage] {
        [migrateV1toV2, migrateV2toV3]  // References V2 but not in schemas
    }
}
swift
enum MigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [SchemaV1.self, SchemaV3.self]  // ❌ Missing V2!
    }

    static var stages: [MigrationStage] {
        [migrateV1toV2, migrateV2toV3]  // References V2 but not in schemas
    }
}

✅ CORRECT (All versions in order)

✅ 正确写法(包含所有版本)

swift
enum MigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [SchemaV1.self, SchemaV2.self, SchemaV3.self]  // ✅ All versions
    }

    static var stages: [MigrationStage] {
        [migrateV1toV2, migrateV2toV3]
    }
}
Time cost 2 minutes to add missing version

swift
enum MigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [SchemaV1.self, SchemaV2.self, SchemaV3.self]  // ✅ All versions
    }

    static var stages: [MigrationStage] {
        [migrateV1toV2, migrateV2toV3]
    }
}
耗时 2分钟添加缺失版本

Pattern 3a: Data Loss from willMigrate/didMigrate Misuse

模式3a:修复willMigrate/didMigrate误用导致的数据丢失

PRINCIPLE Old models only accessible in willMigrate, new models only in didMigrate.
原则 旧模型仅能在willMigrate中访问,新模型仅能在didMigrate中访问。

❌ WRONG (Tries to access old models in didMigrate)

❌ 错误写法(在didMigrate中访问旧模型)

swift
static let migrate = MigrationStage.custom(
    fromVersion: SchemaV1.self,
    toVersion: SchemaV2.self,
    willMigrate: nil,
    didMigrate: { context in
        // ❌ CRASH: SchemaV1.Note doesn't exist here
        let oldNotes = try context.fetch(FetchDescriptor<SchemaV1.Note>())

        // Data lost because transformation never ran
    }
)
swift
static let migrate = MigrationStage.custom(
    fromVersion: SchemaV1.self,
    toVersion: SchemaV2.self,
    willMigrate: nil,
    didMigrate: { context in
        // ❌ CRASH: SchemaV1.Note doesn't exist here
        let oldNotes = try context.fetch(FetchDescriptor<SchemaV1.Note>())

        // Data lost because transformation never ran
    }
)

✅ CORRECT (Transform in willMigrate)

✅ 正确写法(在willMigrate中转换数据)

swift
static let migrate = MigrationStage.custom(
    fromVersion: SchemaV1.self,
    toVersion: SchemaV2.self,
    willMigrate: { context in
        // ✅ SchemaV1.Note exists here
        let oldNotes = try context.fetch(FetchDescriptor<SchemaV1.Note>())

        // Transform data while old models still accessible
        for note in oldNotes {
            note.transformed = transformLogic(note.oldValue)
        }

        try context.save()  // ✅ Save before migration completes
    },
    didMigrate: nil
)
Time cost 5 minutes to move logic to correct closure

swift
static let migrate = MigrationStage.custom(
    fromVersion: SchemaV1.self,
    toVersion: SchemaV2.self,
    willMigrate: { context in
        // ✅ SchemaV1.Note exists here
        let oldNotes = try context.fetch(FetchDescriptor<SchemaV1.Note>())

        // Transform data while old models still accessible
        for note in oldNotes {
            note.transformed = transformLogic(note.oldValue)
        }

        try context.save()  // ✅ Save before migration completes
    },
    didMigrate: nil
)
耗时 5分钟将逻辑移至正确的闭包

Pattern 4a: Real Device Testing

模式4a:真机测试

PRINCIPLE Simulator deletes database on rebuild. Real devices keep persistent databases.
原则 模拟器会在每次重建时删除数据库。真机保留持久化数据库。

Testing Workflow

测试流程

bash
undefined
bash
undefined

1. Install v1 on real device

1. Install v1 on real device

Build with SchemaV1 as current version

Build with SchemaV1 as current version

Run app, create sample data (100+ records)

Run app, create sample data (100+ records)

2. Verify data exists

2. Verify data exists

Check app: should see 100+ records

Check app: should see 100+ records

3. Install v2 with migration

3. Install v2 with migration

Build with SchemaV2 as current version + migration plan

Build with SchemaV2 as current version + migration plan

Install over existing app (don't delete)

Install over existing app (don't delete)

4. Verify migration succeeded

4. Verify migration succeeded

App launches without crash

App launches without crash

Data still exists (100+ records)

Data still exists (100+ records)

Relationships intact

Relationships intact

undefined
undefined

Migration Test Code

迁移测试代码

swift
import Testing
import SwiftData

@Test func testMigrationOnRealDevice() throws {
    // This test MUST run on real device, not simulator
    #if targetEnvironment(simulator)
    throw XCTSkip("Migration test requires real device")
    #endif

    let container = try ModelContainer(
        for: Schema(versionedSchema: SchemaV2.self),
        migrationPlan: MigrationPlan.self
    )

    let context = container.mainContext
    let notes = try context.fetch(FetchDescriptor<SchemaV2.Note>())

    // Verify data preserved
    #expect(notes.count > 0)

    // Verify relationships
    for note in notes {
        if note.folder != nil {
            #expect(note.folder?.notes.contains { $0.id == note.id } == true)
        }
    }
}
Time cost 15 minutes to test on real device

swift
import Testing
import SwiftData

@Test func testMigrationOnRealDevice() throws {
    // This test MUST run on real device, not simulator
    #if targetEnvironment(simulator)
    throw XCTSkip("Migration test requires real device")
    #endif

    let container = try ModelContainer(
        for: Schema(versionedSchema: SchemaV2.self),
        migrationPlan: MigrationPlan.self
    )

    let context = container.mainContext
    let notes = try context.fetch(FetchDescriptor<SchemaV2.Note>())

    // Verify data preserved
    #expect(notes.count > 0)

    // Verify relationships
    for note in notes {
        if note.folder != nil {
            #expect(note.folder?.notes.contains { $0.id == note.id } == true)
        }
    }
}
耗时 15分钟在真机上测试

Pattern 5a: Relationship Prefetching to Preserve Integrity

模式5a:通过预取关联关系保持完整性

PRINCIPLE Fetch relationships eagerly during migration to avoid faulting errors.
原则 迁移期间要预取关联关系以避免错误。

❌ WRONG (Relationships may fault and break)

❌ 错误写法(关联关系可能触发错误)

swift
static let migrate = MigrationStage.custom(
    fromVersion: SchemaV1.self,
    toVersion: SchemaV2.self,
    willMigrate: { context in
        let notes = try context.fetch(FetchDescriptor<SchemaV1.Note>())

        for note in notes {
            // ❌ May trigger fault, relationship not loaded
            let folderName = note.folder?.name
        }
    },
    didMigrate: nil
)
swift
static let migrate = MigrationStage.custom(
    fromVersion: SchemaV1.self,
    toVersion: SchemaV2.self,
    willMigrate: { context in
        let notes = try context.fetch(FetchDescriptor<SchemaV1.Note>())

        for note in notes {
            // ❌ May trigger fault, relationship not loaded
            let folderName = note.folder?.name
        }
    },
    didMigrate: nil
)

✅ CORRECT (Prefetch relationships)

✅ 正确写法(预取关联关系)

swift
static let migrate = MigrationStage.custom(
    fromVersion: SchemaV1.self,
    toVersion: SchemaV2.self,
    willMigrate: { context in
        var fetchDesc = FetchDescriptor<SchemaV1.Note>()

        // ✅ Prefetch relationships
        fetchDesc.relationshipKeyPathsForPrefetching = [\.folder, \.tags]

        let notes = try context.fetch(fetchDesc)

        for note in notes {
            // ✅ Relationships already loaded
            let folderName = note.folder?.name
            let tagCount = note.tags.count
        }

        try context.save()
    },
    didMigrate: nil
)
Time cost 3 minutes to add prefetching

swift
static let migrate = MigrationStage.custom(
    fromVersion: SchemaV1.self,
    toVersion: SchemaV2.self,
    willMigrate: { context in
        var fetchDesc = FetchDescriptor<SchemaV1.Note>()

        // ✅ Prefetch relationships
        fetchDesc.relationshipKeyPathsForPrefetching = [\.folder, \.tags]

        let notes = try context.fetch(fetchDesc)

        for note in notes {
            // ✅ Relationships already loaded
            let folderName = note.folder?.name
            let tagCount = note.tags.count
        }

        try context.save()
    },
    didMigrate: nil
)
耗时 3分钟添加预取配置

Quick Reference: Error → Fix Mapping

快速参考:错误→修复映射

Error MessageRoot CauseFixTime
"Expected only Arrays for Relationships"Many-to-many inverse missingAdd
@Relationship(inverse:)
to both sides
2 min
"The model used to open the store is incompatible"Schema version mismatchAdd missing version to
schemas
array
2 min
"Failed to fulfill faulting for [relationship]"Relationship not prefetchedAdd
relationshipKeyPathsForPrefetching
3 min
App crashes after schema changeMissing model in VersionedSchemaInclude ALL models in
models
array
2 min
Data lost after migrationTransformation in wrong closureMove logic from didMigrate to willMigrate5 min
Simulator works, device crashesUntested migration pathTest on real device with real data15 min
Relationships nil after migrationInverse relationship wrongFix
@Relationship(inverse:)
keypath
3 min

错误信息根本原因修复方案耗时
"Expected only Arrays for Relationships"多对多关联缺失逆属性为双方添加
@Relationship(inverse:)
2分钟
"The model used to open the store is incompatible"架构版本不匹配将缺失版本添加到
schemas
数组
2分钟
"Failed to fulfill faulting for [relationship]"关联关系未预取添加
relationshipKeyPathsForPrefetching
3分钟
架构变更后应用崩溃VersionedSchema中缺失模型将所有模型加入
models
数组
2分钟
迁移后数据丢失转换逻辑在错误的闭包中将逻辑从didMigrate移至willMigrate5分钟
模拟器正常,真机崩溃迁移路径未测试在真机上使用真实数据测试15分钟
迁移后关联关系为nil逆属性配置错误修复
@Relationship(inverse:)
的路径
3分钟

Debugging Checklist

调试清单

When migration fails, verify ALL of these:
  • All models included in
    VersionedSchema.models
    array
  • All schema versions included in
    SchemaMigrationPlan.schemas
    array
  • Migration stages match schema transitions (V1→V2, V2→V3)
  • Many-to-many relationships have explicit
    inverse:
    on both sides
  • Container initialized with correct latest schema version
  • Migration plan registered in
    ModelContainer
    initialization
  • Tested on real device (not just simulator)
  • Tested upgrade path (v1 → v2), not just fresh install
  • SwiftData debug logging enabled (
    -com.apple.coredata.swiftdata.debug 1
    )
  • Data transformation logic in
    willMigrate
    (not
    didMigrate
    )

迁移失败时,验证以下所有项:
  • 所有模型都已包含在
    VersionedSchema.models
    数组中
  • 所有架构版本都已包含在
    SchemaMigrationPlan.schemas
    数组中
  • 迁移阶段与架构转换匹配(V1→V2, V2→V3)
  • 多对多关联的双方都有显式
    inverse:
    配置
  • 容器使用正确的最新架构版本初始化
  • 迁移计划已在
    ModelContainer
    初始化时注册
  • 已在真机上测试(而非仅模拟器)
  • 已测试升级路径(v1 → v2),而非仅全新安装
  • 已启用SwiftData调试日志(
    -com.apple.coredata.swiftdata.debug 1
  • 数据转换逻辑在
    willMigrate
    中(而非
    didMigrate

When You're Stuck After 30 Minutes

当你卡壳超过30分钟时

If you've spent >30 minutes and the migration issue persists:
如果你已花费超过30分钟仍未解决迁移问题:

STOP. You either

停止操作。你可能

  1. Skipped mandatory diagnostics (most common)
  2. Misidentified the actual problem
  3. Applied wrong pattern for your symptom
  4. Haven't tested on real device/real data
  5. Have complex edge case requiring two-stage migration
  1. 跳过了强制诊断步骤(最常见)
  2. 错误识别了实际问题
  3. 应用了不匹配症状的修复模式
  4. 未在真机/真实数据上测试
  5. 存在需要两阶段迁移的复杂边缘情况

MANDATORY checklist before claiming "skill didn't work"

在声称"方案无效"前必须完成的清单

  • I ran all Mandatory First Steps diagnostics
  • I identified the problem type (relationship, schema mismatch, data loss, testing gap)
  • I enabled SwiftData debug logging and examined SQL output
  • I tested on real device with real data (not simulator)
  • I applied the FIRST matching pattern from Decision Tree
  • I verified all models included in VersionedSchema
  • I checked relationship inverse declarations
  • 我已执行所有强制第一步诊断
  • 我已确定问题类型(关联关系、架构不匹配、数据丢失、测试漏洞)
  • 我已启用SwiftData调试日志并检查了SQL输出
  • 我已在真机上使用真实数据测试(而非仅模拟器)
  • 我已应用了决策树中第一个匹配的模式
  • 我已验证所有模型都包含在VersionedSchema中
  • 我已检查了关联关系逆属性的声明

If ALL boxes are checked and still broken

如果所有项都已完成但仍未解决

  • You need two-stage migration (covered in
    axiom-swiftdata-migration
    skill)
  • Time cost: 30-60 minutes for complex type change migration
  • Ask: "What data transformation is actually needed?" and implement two-stage pattern

  • 你需要两阶段迁移(在
    axiom-swiftdata-migration
    方案中涵盖)
  • 耗时:30-60分钟处理复杂类型变更迁移
  • 思考:"实际需要什么样的数据转换?"并实现两阶段模式

Time Cost Transparency

耗时透明度

  • Pattern 1 (relationship inverse): 2-3 minutes
  • Pattern 2 (schema version): 2-5 minutes
  • Pattern 3 (willMigrate fix): 5-10 minutes
  • Pattern 4 (real device testing): 15-30 minutes
  • Pattern 5 (relationship prefetching): 3-5 minutes

  • 模式1(关联关系逆属性):2-3分钟
  • 模式2(架构版本):2-5分钟
  • 模式3(willMigrate修复):5-10分钟
  • 模式4(真机测试):15-30分钟
  • 模式5(关联关系预取):3-5分钟

Real-World Impact

实际业务影响

Before SwiftData migration debugging 2-8 hours per issue
  • App crashes on launch in production
  • Data loss for existing users
  • Relationships broken after migration
  • Simulator success, device failure
  • Customer trust damaged
After 15-45 minutes with systematic diagnosis
  • Identify problem type with diagnostics (5 min)
  • Apply correct pattern (5-10 min)
  • Test on real device (15-30 min)
  • Deploy with confidence
Key insight SwiftData has well-established patterns for every common migration issue. The problem is developers don't know which diagnostic applies to their error.

之前 调试SwiftData迁移问题需2-8小时/次
  • 生产环境应用启动崩溃
  • 现有用户数据丢失
  • 迁移后关联关系损坏
  • 模拟器正常但真机崩溃
  • 客户信任受损
之后 系统化诊断仅需15-45分钟
  • 5分钟诊断问题类型
  • 5-10分钟应用正确修复模式
  • 15-30分钟在真机上测试
  • 自信地部署版本
关键洞察 SwiftData针对每一种常见迁移问题都有成熟的修复模式。问题在于开发者不知道哪种诊断适用于他们遇到的错误。

Resources

资源

WWDC: 2025-291, 2023-10195
Docs: /swiftdata
Skills: axiom-swiftdata-migration, axiom-swiftdata, axiom-database-migration

Created 2025-12-09 Status Production-ready diagnostic patterns Framework SwiftData (Apple) Swift 5.9+
WWDC: 2025-291, 2023-10195
文档: /swiftdata
相关方案: axiom-swiftdata-migration, axiom-swiftdata, axiom-database-migration

创建时间 2025-12-09 状态 生产可用的诊断模式 框架 SwiftData (Apple) Swift版本 5.9+