axiom-swiftdata-migration-diag by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-swiftdata-migration-diagSwiftData 迁移失败表现为生产环境崩溃、数据丢失、关系损坏或仅在模拟器中成功。核心原则 90% 的迁移失败源于 VersionedSchema 中缺少模型、关系反向声明问题或未经测试的迁移路径——而非 SwiftData 框架本身的错误。
如果出现以下任何情况,请怀疑迁移配置问题:
关键区别 模拟器在每次重建时都会删除数据库,从而隐藏了架构不匹配问题。真实设备保留持久化数据库,并在架构不匹配时立即崩溃。强制要求:在发布前,使用真实数据在真实设备上测试迁移。
始终首先运行这些步骤(在更改代码之前):
// 1. 识别崩溃/问题类型
// 截图崩溃信息并记录:
// - "Expected only Arrays" = 关系反向声明缺失
// - "incompatible model" = 架构版本不匹配
// - "Failed to fulfill faulting" = 关系完整性损坏
// - 模拟器有效,设备崩溃 = 未经测试的迁移路径
// 记录:"Error type: [exact message]"
// 2. 检查架构版本配置
// 在迁移计划中:
enum MigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
// ✅ 验证:所有版本是否按顺序排列?
// ✅ 验证:最新版本是否与容器匹配?
[SchemaV1.self, SchemaV2.self, SchemaV3.self]
}
static var stages: [MigrationStage] {
// ✅ 验证:迁移阶段是否与架构转换匹配?
[migrateV1toV2, migrateV2toV3]
}
}
// 在应用中:
let schema = Schema(versionedSchema: SchemaV3.self) // ✅ 验证:是否与计划中的最新版本匹配?
let container = try ModelContainer(
for: schema,
migrationPlan: MigrationPlan.self // ✅ 验证:计划是否已注册?
)
// 记录:"Schema version: latest is [version]"
// 3. 检查所有模型是否包含在 VersionedSchema 中
enum SchemaV2: VersionedSchema {
static var models: [any PersistentModel.Type] {
// ✅ 验证:是否列出了所有模型?(即使是未更改的模型)
[Note.self, Folder.self, Tag.self]
}
}
// 记录:"Missing models? Yes/no"
// 4. 检查关系反向声明
@Model
final class Note {
@Relationship(deleteRule: .nullify, inverse: \Folder.notes) // ✅ 验证:是否指定了反向关系?
var folder: Folder?
@Relationship(deleteRule: .nullify, inverse: \Tag.notes) // ✅ 验证:是否指定了反向关系?
var tags: [Tag] = []
}
// 记录:"Relationship inverses: all specified? Yes/no"
// 5. 启用 SwiftData 调试日志
// 在 Xcode scheme 中,添加参数:
// -com.apple.coredata.swiftdata.debug 1
// 运行并检查控制台中的 SQL 查询
// 记录:"Debug log shows: [what you see]"
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
在更改任何代码之前,识别以下情况之一:
-com.apple.coredata.swiftdata.debug 1 并检查 SQL 输出当迁移看似完成且没有错误,但你想验证数据完整性时,请使用此部分。
迁移运行且未崩溃后:
// 1. 验证记录数量与迁移前匹配
let context = container.mainContext
let postMigrationCount = try context.fetch(FetchDescriptor<Note>()).count
print("Post-migration count: \(postMigrationCount)")
// 与迁移前数量比较
// 2. 抽查特定记录
let sampleNote = try context.fetch(
FetchDescriptor<Note>(predicate: #Predicate { $0.id == "known-test-id" })
).first
print("Sample note title: \(sampleNote?.title ?? "MISSING")")
// 3. 验证关系完整
if let note = sampleNote {
print("Folder relationship: \(note.folder != nil ? "✓" : "✗")")
print("Tags count: \(note.tags.count)")
// 验证反向关系
if let folder = note.folder {
let folderHasNote = folder.notes.contains { $0.id == note.id }
print("Inverse relationship: \(folderHasNote ? "✓" : "✗")")
}
}
// 4. 检查孤立数据
let orphanedNotes = try context.fetch(
FetchDescriptor<Note>(predicate: #Predicate { $0.folder == nil })
)
print("Orphaned notes (should be 0 if cascade delete worked): \(orphanedNotes.count)")
控制台输出:
Post-migration count: 1523 // 与迁移前匹配
Sample note title: Test Note // 不是 "MISSING"
Folder relationship: ✓
Tags count: 3
Inverse relationship: ✓
Orphaned notes: 0
如果你看到:
有关具体修复方法,请参见以下模式。
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)
原则 多对多关系需要显式的反向声明。
@Model
final class Note {
var tags: [Tag] = [] // ❌ 缺少反向关系
}
@Model
final class Tag {
var notes: [Note] = [] // ❌ 缺少反向关系
}
@Model
final class Note {
@Relationship(deleteRule: .nullify, inverse: \Tag.notes)
var tags: [Tag] = [] // ✅ 指定了反向关系
}
@Model
final class Tag {
@Relationship(deleteRule: .nullify, inverse: \Note.tags)
var notes: [Note] = [] // ✅ 指定了反向关系
}
为什么有效 SwiftData 要求为多对多关系显式指定反向关系,以正确创建连接表。
时间成本 2 分钟添加反向声明
原则 在 iOS 17.0 中,如果模型名称按字母顺序排列,多对多关系可能会失败。
@Model
final class Actor {
@Relationship(deleteRule: .nullify, inverse: \Movie.actors)
var movies: [Movie] // ❌ 无默认值
}
@Model
final class Movie {
@Relationship(deleteRule: .nullify, inverse: \Actor.movies)
var actors: [Actor] // ❌ 无默认值
}
// 如果 "Actor" < "Movie" 按字母顺序排列,则崩溃
@Model
final class Actor {
@Relationship(deleteRule: .nullify, inverse: \Movie.actors)
var movies: [Movie] = [] // ✅ 默认值
}
@Model
final class Movie {
@Relationship(deleteRule: .nullify, inverse: \Actor.movies)
var actors: [Actor] = [] // ✅ 默认值
}
修复于 iOS 17.1+
时间成本 1 分钟添加默认值
原则 迁移计划的 schemas 数组必须按顺序包含所有版本。
enum MigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[SchemaV1.self, SchemaV3.self] // ❌ 缺少 V2!
}
static var stages: [MigrationStage] {
[migrateV1toV2, migrateV2toV3] // 引用了 V2 但未包含在 schemas 中
}
}
enum MigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[SchemaV1.self, SchemaV2.self, SchemaV3.self] // ✅ 所有版本
}
static var stages: [MigrationStage] {
[migrateV1toV2, migrateV2toV3]
}
}
时间成本 2 分钟添加缺失的版本
原则 旧模型仅在 willMigrate 中可访问,新模型仅在 didMigrate 中可访问。
static let migrate = MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self,
willMigrate: nil,
didMigrate: { context in
// ❌ 崩溃:SchemaV1.Note 在此不存在
let oldNotes = try context.fetch(FetchDescriptor<SchemaV1.Note>())
// 数据丢失,因为转换从未运行
}
)
static let migrate = MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self,
willMigrate: { context in
// ✅ SchemaV1.Note 在此存在
let oldNotes = try context.fetch(FetchDescriptor<SchemaV1.Note>())
// 在旧模型仍可访问时转换数据
for note in oldNotes {
note.transformed = transformLogic(note.oldValue)
}
try context.save() // ✅ 在迁移完成前保存
},
didMigrate: nil
)
时间成本 5 分钟将逻辑移动到正确的闭包
原则 模拟器在重建时删除数据库。真实设备保留持久化数据库。
# 1. 在真实设备上安装 v1
# 使用 SchemaV1 作为当前版本构建
# 运行应用,创建示例数据(100+ 条记录)
# 2. 验证数据存在
# 检查应用:应看到 100+ 条记录
# 3. 安装带有迁移的 v2
# 使用 SchemaV2 作为当前版本 + 迁移计划构建
# 覆盖现有应用安装(不要删除)
# 4. 验证迁移成功
# 应用启动无崩溃
# 数据仍然存在(100+ 条记录)
# 关系完整
import Testing
import SwiftData
@Test func testMigrationOnRealDevice() throws {
// 此测试必须在真实设备上运行,而非模拟器
#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>())
// 验证数据保留
#expect(notes.count > 0)
// 验证关系
for note in notes {
if note.folder != nil {
#expect(note.folder?.notes.contains { $0.id == note.id } == true)
}
}
}
时间成本 15 分钟在真实设备上测试
原则 在迁移期间急切获取关系,以避免错误。
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 {
// ❌ 可能触发错误,关系未加载
let folderName = note.folder?.name
}
},
didMigrate: nil
)
static let migrate = MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self,
willMigrate: { context in
var fetchDesc = FetchDescriptor<SchemaV1.Note>()
// ✅ 预取关系
fetchDesc.relationshipKeyPathsForPrefetching = [\.folder, \.tags]
let notes = try context.fetch(fetchDesc)
for note in notes {
// ✅ 关系已加载
let folderName = note.folder?.name
let tagCount = note.tags.count
}
try context.save()
},
didMigrate: nil
)
时间成本 3 分钟添加预取
| 错误信息 | 根本原因 | 修复方法 | 时间 |
|---|---|---|---|
| "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 移动到 willMigrate | 5 分钟 |
| 模拟器有效,设备崩溃 | 未经测试的迁移路径 | 使用真实数据在真实设备上测试 | 15 分钟 |
| 迁移后关系为 nil | 反向关系错误 | 修复 @Relationship(inverse:) 键路径 | 3 分钟 |
当迁移失败时,验证所有以下内容:
VersionedSchema.models 数组中SchemaMigrationPlan.schemas 数组中inverse:ModelContainer 初始化中注册-com.apple.coredata.swiftdata.debug 1)willMigrate 中(不在 didMigrate 中)如果你花费了 >30 分钟且迁移问题仍然存在:
axiom-swiftdata-migration 技能中涵盖)之前 SwiftData 迁移调试每个问题需要 2-8 小时
之后 使用系统化诊断需要 15-45 分钟
关键洞察 SwiftData 为每个常见的迁移问题都建立了成熟的模式。问题在于开发者不知道哪种诊断适用于他们的错误。
WWDC : 2025-291, 2023-10195
文档 : /swiftdata
技能 : axiom-swiftdata-migration, axiom-swiftdata, axiom-database-migration
创建于 2025-12-09 状态 生产就绪的诊断模式 框架 SwiftData (Apple) Swift 5.9+
每周安装次数
99
代码仓库
GitHub 星标数
606
首次出现
Jan 21, 2026
安全审计
安装于
opencode83
claude-code78
codex77
gemini-cli76
cursor76
github-copilot72
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.
If you see ANY of these, suspect a migration configuration problem:
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.
ALWAYS run these FIRST (before changing code):
// 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]"
Before changing ANY code, identify ONE of these:
-com.apple.coredata.swiftdata.debug 1 and examine SQL outputUse this section when migration appears to complete without errors, but you want to verify data integrity.
After migration runs without crashing:
// 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)")
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:
See patterns below for specific fixes.
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)
PRINCIPLE Many-to-many relationships require explicit inverse declarations.
@Model
final class Note {
var tags: [Tag] = [] // ❌ Missing inverse
}
@Model
final class Tag {
var notes: [Note] = [] // ❌ Missing inverse
}
@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
PRINCIPLE In iOS 17.0, many-to-many relationships could fail if model names were in alphabetical order.
@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
@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
PRINCIPLE Migration plan's schemas array must include ALL versions in order.
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
}
}
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
PRINCIPLE Old models only accessible in willMigrate, new models only in didMigrate.
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
}
)
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
PRINCIPLE Simulator deletes database on rebuild. Real devices keep persistent databases.
# 1. Install v1 on real device
# Build with SchemaV1 as current version
# Run app, create sample data (100+ records)
# 2. Verify data exists
# Check app: should see 100+ records
# 3. Install v2 with migration
# Build with SchemaV2 as current version + migration plan
# Install over existing app (don't delete)
# 4. Verify migration succeeded
# App launches without crash
# Data still exists (100+ records)
# Relationships intact
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
PRINCIPLE Fetch relationships eagerly during migration to avoid faulting errors.
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
)
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
| Error Message | Root Cause | Fix | Time |
|---|---|---|---|
| "Expected only Arrays for Relationships" | Many-to-many inverse missing | Add @Relationship(inverse:) to both sides | 2 min |
| "The model used to open the store is incompatible" | Schema version mismatch | Add missing version to schemas array | 2 min |
| "Failed to fulfill faulting for [relationship]" | Relationship not prefetched | Add relationshipKeyPathsForPrefetching | 3 min |
| App crashes after schema change | Missing model in VersionedSchema | Include ALL models in models array |
When migration fails, verify ALL of these:
VersionedSchema.models arraySchemaMigrationPlan.schemas arrayinverse: on both sidesModelContainer initialization-com.apple.coredata.swiftdata.debug 1)willMigrate (not didMigrate)If you've spent >30 minutes and the migration issue persists:
axiom-swiftdata-migration skill)Before SwiftData migration debugging 2-8 hours per issue
After 15-45 minutes with systematic diagnosis
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.
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+
Weekly Installs
99
Repository
GitHub Stars
606
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode83
claude-code78
codex77
gemini-cli76
cursor76
github-copilot72
SQL查询优化指南:PostgreSQL、Snowflake、BigQuery高性能SQL编写技巧与方言参考
977 周安装
| 2 min |
| Data lost after migration | Transformation in wrong closure | Move logic from didMigrate to willMigrate | 5 min |
| Simulator works, device crashes | Untested migration path | Test on real device with real data | 15 min |
| Relationships nil after migration | Inverse relationship wrong | Fix @Relationship(inverse:) keypath | 3 min |