Use when SwiftData migrations crash, fail to preserve relationships, lose data, or work in simulator but fail on device - systematic diagnostics for schema version mismatches, relationship errors, and migration testing gaps
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
Use when SwiftData migrations crash, fail to preserve relationships, lose data, or work in simulator but fail on device - systematic diagnostics for schema version mismatches, relationship errors, and migration testing gaps
license
MIT
metadata
{"version":"1.0.0"}
SwiftData Migration Diagnostics
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.
Red Flags — Suspect SwiftData Migration Issue
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.
Mandatory First Steps
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:enumMigrationPlan: SchemaMigrationPlan {
staticvar schemas: [anyVersionedSchema.Type] {
// ✅ VERIFY: All versions in order?// ✅ VERIFY: Latest version matches container?
[SchemaV1.self, SchemaV2.self, SchemaV3.self]
}
staticvar 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 =tryModelContainer(
for: schema,
migrationPlan: MigrationPlan.self// ✅ VERIFY: Plan is registered?
)
// Record: "Schema version: latest is [version]"// 3. Check all models included in VersionedSchemaenumSchemaV2: VersionedSchema {
staticvar models: [anyPersistentModel.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@ModelfinalclassNote {
@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)
Pattern 3a: Data Loss from willMigrate/didMigrate Misuse
PRINCIPLE Old models only accessible in willMigrate, new models only in didMigrate.
❌ WRONG (Tries to access old models in didMigrate)
staticlet migrate =MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self,
willMigrate: nil,
didMigrate: { context in// ❌ CRASH: SchemaV1.Note doesn't exist herelet oldNotes =try context.fetch(FetchDescriptor<SchemaV1.Note>())
// Data lost because transformation never ran
}
)
✅ CORRECT (Transform in willMigrate)
staticlet migrate =MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self,
willMigrate: { context in// ✅ SchemaV1.Note exists herelet oldNotes =try context.fetch(FetchDescriptor<SchemaV1.Note>())
// Transform data while old models still accessiblefor 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
Pattern 4a: Real Device Testing
PRINCIPLE Simulator deletes database on rebuild. Real devices keep persistent databases.
Testing Workflow
# 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
Migration Test Code
import Testing
import SwiftData
@TestfunctestMigrationOnRealDevice() throws {
// This test MUST run on real device, not simulator#if targetEnvironment(simulator)
throwXCTSkip("Migration test requires real device")
#endiflet container =tryModelContainer(
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 relationshipsfor 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
Pattern 5a: Relationship Prefetching to Preserve Integrity
PRINCIPLE Fetch relationships eagerly during migration to avoid faulting errors.
❌ WRONG (Relationships may fault and break)
staticlet migrate =MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self,
willMigrate: { context inlet notes =try context.fetch(FetchDescriptor<SchemaV1.Note>())
for note in notes {
// ❌ May trigger fault, relationship not loadedlet folderName = note.folder?.name
}
},
didMigrate: nil
)
Data transformation logic in willMigrate (not didMigrate)
When You're Stuck After 30 Minutes
If you've spent >30 minutes and the migration issue persists:
STOP. You either
Skipped mandatory diagnostics (most common)
Misidentified the actual problem
Applied wrong pattern for your symptom
Haven't tested on real device/real data
Have complex edge case requiring two-stage migration
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
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
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
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.