クラウドMacでiOSデータベース移行の回帰ゲートを構築する

クラウドMacでiOSデータベース移行の回帰ゲートを構築する

一見すると単純な Core Data のフィールド変更でも、新規インストールしたシミュレータでは問題なく動作する一方、旧バージョンから直接アップデートしたユーザーが起動画面から進めなくなることがあります。CI で本当に検証すべきなのは「空のストアを作成できるか」ではなく、旧ストアをサポート対象の経路で移行できるか、重要なレコードの整合性が保たれるか、そして失敗時に十分な証拠を残せるかです。常時稼働するクラウド Mac は、このように時間はかかっても結果が確定的なチェックを独立した回帰ゲートとして運用するのに適しています。

まず移行のサポート範囲を定義する

最初に、現存する可能性があるデータベースのバージョンを洗い出します。ユーザーが必ず1バージョンずつ更新するとは限りません。現在のモデルが V5 で、V3、V4 のクライアントがまだ利用されているなら、少なくとも V3→V5 と V4→V5 をテストします。V5 の空ストアを使ったテストで確認できるのは、現行モデルを読み込めることだけです。旧フィールドの名称変更、リレーション制約の変更、一意インデックスの競合までは検証できません。

次のような簡潔なマトリクスを管理しておくとよいでしょう。

サンプル アップグレード先 必須確認項目
V3 基本ストア V5 アカウント、プロジェクト、過去のタスク数
V4 境界値ストア V5 空のリレーション、重複名、削除済みフラグ
V4 大規模サンプル V5 移行完了までの時間と最大ファイルサイズ
V5 空ストア V5 現行モデルの初期化

移行ゲートの合格条件は、永続化コンテナがエラーを返さないことだけではなく、「データの意味的整合性が保たれていること」であるべきです。

各サンプルには、それを生成したアプリのバージョン、モデル識別子、想定レコード数、検証用の要約も記録します。開発者が日常的に使っているデータベースからその場でサンプルを採取してはいけません。操作のたびに内容が変化し、失敗を再現できなくなるためです。

再現可能な旧ストアのサンプルを固定する

過去の各バージョン向けに専用ビルドを用意し、固定データを書き込みます。通常のレコードだけでなく、NULL 値、長文、アーカイブ済みオブジェクト、制約の境界に近いオブジェクトも含めます。書き込み後は永続化コンテナを正常に閉じてから、データベースをコピーしてください。

SQLite で WAL を使用している場合、データが3つのファイルに分散していることがあります。最も確実なのは、アプリですべての保存を完了して checkpoint を実行する方法です。それを保証できない場合は、ファイル一式をまとめて保存します。

set -euo pipefail

STORE="$HOME/Library/Developer/CoreSimulator/Devices/$SIM_UDID/data/Containers/Data/Application/$APP_ID/Library/Application Support/App.sqlite"
OUT="MigrationFixtures/V4"

mkdir -p "$OUT"
cp "$STORE" "$OUT/App.sqlite"

for suffix in -wal -shm; do
  if [ -f "${STORE}${suffix}" ]; then
    cp "${STORE}${suffix}" "$OUT/App.sqlite${suffix}"
  fi
done

find "$OUT" -type f -print0 | sort -z | xargs -0 shasum -a 256 > "$OUT/SHA256SUMS"

テストでは、リポジトリ内の元サンプルを直接開かないでください。各テストの開始時に一時ディレクトリへコピーし、移行完了後に削除します。これにより、並列タスクが同じサンプルを書き換える事態を防げます。サンプルに実ユーザーのデータ、トークン、接続情報が含まれる場合は、後からマスキングするのではなく、匿名データを使って作り直してください。

移行処理をテスト可能な入口として分離する

アプリの起動処理では、データベースの読み込み、UI の初期化、ネットワークリクエストが密結合になりがちです。そのままでは、移行失敗の原因を特定しにくくなります。永続化コンテナの作成処理を、URL を注入できるコンポーネントとして切り出し、XCTest から一時コピーを直接読み込めるようにします。

func makeContainer(storeURL: URL) throws -> NSPersistentContainer {
    let container = NSPersistentContainer(name: "AppModel")
    let description = NSPersistentStoreDescription(url: storeURL)
    description.shouldMigrateStoreAutomatically = true
    description.shouldInferMappingModelAutomatically = true
    description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
    container.persistentStoreDescriptions = [description]

    var loadError: Error?
    container.loadPersistentStores { _, error in
        loadError = error
    }
    if let loadError {
        throw loadError
    }
    return container
}

軽量マイグレーションが適しているのは、任意項目の追加、デフォルト値を持つ属性の追加、明示的な名称変更などです。エンティティの分割、データの統合、値の変換が伴う場合は、明示的なマッピングまたは段階的な移行を用意します。テストを通すために旧モデルを削除してはいけません。過去のモデルは、旧ストアのメタデータを読み取り、移行経路を推論するために必要な入力です。

ビジネス上の不変条件を検証する

移行後は、少なくとも4つの層を確認します。永続ストアが正常に読み込まれたこと、モデルのバージョンが更新されたこと、レコード数が想定どおりであること、重要なリレーションとフィールドの意味が正しいことです。たとえば、タスクの総数は変わってはならず、アーカイブ済みのプロジェクトは引き続きアーカイブ済みである必要があり、親を持たない子オブジェクトの数はゼロでなければなりません。

比較時に、自動生成されたオブジェクト ID だけに依存するのは避けます。テストデータには安定したビジネスキーを書き込み、そのキーを基準に各フィールドを検証します。並び順や集合のリレーションは、比較前に正規化し、意味のない順序差による偶発的な失敗を防いでください。

クラウド Mac で独立したテストジョブを実行する

移行テストは、通常のユニットテストとは分離して実行します。サンプルのコピーと永続ストアの作成を何度も行うためです。シミュレータのランタイムと対象デバイス名を固定し、デバイスを起動してから指定のテストプランを実行します。

set -euo pipefail

xcrun simctl bootstatus "$SIM_UDID" -b

xcodebuild test \
  -workspace App.xcworkspace \
  -scheme App \
  -testPlan DatabaseMigration \
  -destination "platform=iOS Simulator,id=$SIM_UDID" \
  -resultBundlePath Artifacts/MigrationTests.xcresult \
  CODE_SIGNING_ALLOWED=NO

テストジョブの開始前に一時ディレクトリをクリーンアップし、終了後は成否にかかわらず xcresult、移行ログ、サンプルの要約、移行先モデルの識別子をアーカイブします。移行後の完全なデータベースを標準の成果物としてアップロードしてはいけません。テスト用の機密情報が含まれる可能性があり、ストレージ使用量も急増します。通常は失敗時に限って匿名化済みサンプルのコピーを保存し、明確な削除期限を設定します。

ZoomMini で実行する場合は、ジョブを特定の Xcode ツールチェーンに固定し、現在選択可能な構成をコンソールで確認します。Xcode またはシミュレータのランタイムを変更した後は、まず移行プランだけを実行し、その後で完全なパイプラインへ戻します。ツールチェーンの変更とモデルの変更を同時に取り込むと、問題の原因を切り分けにくくなります。

よくある失敗とリリース前チェック項目

「ローカルでは成功するが CI では失敗する」問題は、多くの場合、ファイル一式のコピー漏れ、前回のテストによるサンプルの変更、テストバンドルへのモデルリソースの追加漏れ、または複数のテストによる同一パスの同時操作が原因です。まず SHA-256 を照合し、次に一時ディレクトリがジョブごとに一意であることを確認し、最後にビルド成果物へサポート対象の全モデルバージョンが含まれているかを確認します。

リリース前に、次の項目を1つずつ確認します。

  • サポート対象の各開始バージョンに、固定サンプルと想定される要約がある。
  • SQLite のメインファイルと WAL の状態が一致している。
  • テストは一時コピーだけを移行し、元の fixture を変更しない。
  • 軽量マイグレーションと明示的マッピングの適用範囲が明文化されている。
  • レコード数、安定したビジネスキー、リレーション、デフォルト値に対するアサーションがある。
  • 失敗したジョブで xcresult と匿名化済みの移行ログが保持される。
  • モデルバージョンの削除、フィールド名の変更、制約の変更によって移行プランが必ず実行される。
  • 完全なテストでは、サポート対象の最古バージョンから現行バージョンへ直接アップグレードする。

データベース移行は、一度きりのリリーススクリプトではありません。モデルの進化とともに拡張される互換性契約です。過去のサンプル、ビジネス上の不変条件、失敗時の証拠を固定しておけば、モデルを変更するたびに、マージ前に同じ問いへ答えられます。既存データは新しいバージョンへ安全に到達できるのか、という問いです。

よくある質問

移行テストには何世代のデータベースが必要ですか?

最低でも現在公開中の版と、そこへ直接更新できる直前版を保存します。複数リリースを飛ばす更新を許容する場合は、対応する全開始版を追加します。

SQLite本体だけをコピーしてはいけない理由は何ですか?

WALモードでは確定済みデータが-walファイルに残る場合があります。安全にcheckpointするか、sqlite、sqlite-wal、sqlite-shmを一組で保存してください。

ZoomMini クラウドMac

ビルド、テスト、実験に専用物理ノードを選択

2種類のM4構成を、シンガポール、東京、ソウル、香港のノードで提供しています。実際の利用可否は、注文時に返される結果をご確認ください。

モデルを選んで注文