mcpField
A
axiom-swiftdata-migration-diag

axiom-swiftdata-migration-diag

github.com/charleswiltgenデータ・データベース1.2k174 回インストール今日更新
リポジトリ

axiom-swiftdata-migration-diag について

SwiftData の移行失敗を調査する開発者向けスキルです。よくあるエラーをスキーマ、リレーション、データ変換、テストの問題に分類し、確認手順と修正パターンを示します。

主な機能

  1. 01エラーと原因の診断チェックリスト
  2. 02スキーマバージョンとモデルの確認
  3. 03リレーションの逆参照設定ガイド
  4. 04移行後のデータ整合性検証
  5. 05実機での移行テスト

活用シーン

  1. 01スキーマ変更後のクラッシュ調査
  2. 02移行後のレコード欠落の調査
  3. 03リレーション切れや faulting エラーの確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-swiftdata-migration-diag

インストール後、Claude Code で/axiom-swiftdata-migration-diagを入力して読み込まれたか確認します。

詳細

SwiftData のマイグレーション診断

概要

SwiftData のマイグレーション失敗は、本番環境でのクラッシュ、データ損失、リレーションシップの破損、またはシミュレーターでのみ成功するといった形で現れます。基本原則 マイグレーション失敗の 90% は、VersionedSchema にモデルが含まれていないこと、リレーションシップの逆参照に関する問題、または未テストのマイグレーション経路が原因であり、SwiftData のバグではありません。

危険信号 — SwiftData のマイグレーション問題を疑う

次のいずれかが見られる場合は、マイグレーション設定の問題を疑ってください。

  • スキーマ変更後、アプリの起動時にクラッシュする
  • "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 は本番アプリで何百万件ものマイグレーションを処理しています
    • スキーマの不一致やリレーションシップのエラーは、常に設定が原因であり、フレームワークの問題ではありません
    • 問題を正当化して片付けず、原因を診断してください

重要な違い シミュレーターではビルドのたびにデータベースが削除されるため、スキーマ不一致の問題が隠れます。実機ではデータベースが保持され、スキーマ不一致があるとすぐにクラッシュします。必須:リリース前に、実データを使って実機でマイグレーションをテストしてください。

必須の最初の手順

コードを変更する前に、必ず最初に次を実行してください。

// 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]"
これでわかること
  • "Expected only Arrays for Relationships" → パターン 1(リレーションシップの逆参照を修正)へ進みます
  • "incompatible model" → パターン 2(スキーマバージョンの不一致)へ進みます
  • VersionedSchema にモデルがない → パターン 3(完全なスキーマスナップショット)へ進みます
  • シミュレーターでは動作するが、実機ではクラッシュする → パターン 4(マイグレーションのテスト)へ進みます
  • マイグレーション後にデータが失われた → パターン 5(willMigrate/didMigrate の誤用)へ進みます
必須の判断

コードを変更する前に、必ず次のいずれかを特定してください。

  1. エラーが "Expected only Arrays" で、かつリレーションシップの逆参照がない場合 → リレーションシップ設定の問題
  2. エラーに "incompatible" が含まれ、かつスキーマバージョンが一致しない場合 → バージョンの不一致
  3. VersionedSchema からモデルが欠落している場合 → 不完全なスキーマスナップショット
  4. シミュレーターでは成功するが、実機では失敗する場合 → 未テストのマイグレーション経路
  5. マイグレーション前にはデータが存在するが、後には存在しない場合 → willMigrate/didMigrate の制限に違反
診断結果に矛盾がある場合、または不明瞭な場合
  • ここで停止してください。まだパターンに進まないでください
  • -com.apple.coredata.swiftdata.debug 1 を追加して、SQL 出力を確認してください
  • ファイルシステムを確認してください。.sqlite ファイルは存在しますか?サイズはいくつですか?
  • 基準値を確認してください。実際に何が起きているか、想定していた内容とどう違うかを調べてください

マイグレーションの正常完了を確認する

マイグレーションがエラーなしで完了したように見えるものの、データの整合性を確認したい場合は、このセクションを使ってください。

簡単な確認チェックリスト

マイグレーションがクラッシュせずに実行された後:

// 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)")
マイグレーションが成功した場合の状態

コンソール出力:

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 より大きい → カスケード削除ルールが機能していません

具体的な修正方法については、以下のパターンを参照してください。


判断ツリー

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)

よくあるパターン

パターン 1a:"Expected only Arrays for Relationships" を修正する

原則 多対多リレーションシップでは、逆参照を明示的に宣言する必要があります。

❌ 誤り("Expected only Arrays" エラーの原因)
@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
}

これが有効な理由 SwiftData では、多対多リレーションシップの結合テーブルを正しく作成するために、逆参照を明示する必要があります。

所要時間 逆参照の宣言を追加するのに 2 分


パターン 1b:iOS 17.0 のアルファベット順バグへの回避策

原則 iOS 17.0 では、モデル名がアルファベット順に並んでいると、多対多リレーションシップが失敗する場合があります。

❌ 誤り(iOS 17.0 でクラッシュします)
@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
✅ 正しい(iOS 17.0 以降で動作します)
@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 分


パターン 2a:スキーマバージョンの不一致

原則 マイグレーションプランの schemas 配列には、すべてのバージョンを順番に含める必要があります。

❌ 誤り(バージョンが欠けているとクラッシュします)
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]
    }
}

所要時間 欠けているバージョンの追加に 2 分


パターン 3a:willMigrate/didMigrate の誤用によるデータ損失

原則 古いモデルにアクセスできるのは willMigrate 内のみで、新しいモデルにアクセスできるのは didMigrate 内のみです。

❌ 誤り(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
    }
)
✅ 正しい(willMigrate 内で変換します)
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 分


パターン 4a:実機テスト

原則 シミュレーターではビルドし直すとデータベースが削除されます。実機ではデータベースが保持されます。

テストワークフロー
# 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)
        }
    }
}

所要時間 実機でのテストに 15 分


パターン 5a:整合性を保つためのリレーションシップの事前読み込み

原則 フォルトエラーを避けるため、マイグレーション中にリレーションシップを先読みします。

❌ 誤り(リレーションシップがフォルトし、問題が発生する可能性があります)
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
)

所要時間 事前読み込みの追加に 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 配列に含まれている
  • マイグレーションステージがスキーマの移行(V1→V2、V2→V3)に対応している
  • 多対多のリレーションシップでは、両側に inverse: が明示的に指定されている
  • コンテナーが最新の正しいスキーマバージョンで初期化されている
  • ModelContainer の初期化時にマイグレーションプランが登録されている
  • 実機でテスト済み(シミュレーターだけではない)
  • 新規インストールだけでなく、アップグレードパス(v1 → v2)もテスト済み
  • SwiftData のデバッグログが有効になっている(-com.apple.coredata.swiftdata.debug 1)
  • データ変換ロジックが willMigrate 内にある(didMigrate ではない)

30 分経っても行き詰まっている場合

30 分以上かけてもマイグレーションの問題が解決しない場合:

いったん中断してください。次のいずれかです
  1. 必須の診断を省略した(最もよくある原因)
  2. 実際の問題を誤って特定した
  3. 症状に合わないパターンを適用した
  4. 実機または実データでテストしていない
  5. 2 段階のマイグレーションが必要な複雑なエッジケースがある
「スキルが機能しなかった」と判断する前に必ず確認する項目
  • 必須の最初の手順にあるすべての診断を実行した
  • 問題の種類(リレーションシップ、スキーマの不一致、データ損失、テスト不足)を特定した
  • SwiftData のデバッグログを有効にし、SQL 出力を確認した
  • 実データを使って実機でテストした(シミュレーターではない)
  • 判断ツリーで最初に該当するパターンを適用した
  • すべてのモデルが VersionedSchema に含まれていることを確認した
  • リレーションシップの逆指定を確認した
すべてのボックスにチェックが入っているのに、まだ動かない場合
  • 2 段階のマイグレーションが必要です(axiom-swiftdata-migration スキルで解説しています)
  • 所要時間:複雑な型変更のマイグレーションでは 30~60 分
  • 「実際に必要なデータ変換は何か」を確認し、2 段階のパターンを実装してください

所要時間の目安

  • パターン 1(リレーションシップの逆参照):2~3 分
  • パターン 2(スキーマバージョン):2~5 分
  • パターン 3(willMigrate の修正):5~10 分
  • パターン 4(実機テスト):15~30 分
  • パターン 5(リレーションシップの事前読み込み):3~5 分

実際の効果

以前 SwiftData のマイグレーションのデバッグに 1 件あたり 2~8 時間

  • 本番環境でアプリの起動時にクラッシュ
  • 既存ユーザーのデータ損失
  • マイグレーション後にリレーションシップが破損
  • シミュレーターでは成功するが、実機では失敗
  • 顧客の信頼を損なう

導入後 系統的な診断により 15~45 分

  • 診断によって問題の種類を特定(5 分)
  • 適切なパターンを適用(5~10 分)
  • 実機でテスト(15~30 分)
  • 自信を持ってデプロイ

重要なポイント SwiftData には、よくあるあらゆるマイグレーションの問題に対する、確立されたパターンがあります。問題は、開発者がエラーに対してどの診断を適用すべきか分からないことです。


リソース

WWDC:2025-291、2023-10195

ドキュメント:/swiftdata

スキル:axiom-swiftdata-migration、axiom-swiftdata、axiom-database-migration


作成日 2025-12-09 ステータス 本番環境で利用可能な診断パターン フレームワーク SwiftData(Apple) Swift 5.9+

コメント