ARS
ARS

Reputation: 387

SwiftData migration sometimes ignores custom SchemaMigrationPlan

I'm trying to do a custom migration for my SwiftData data models as the model includes custom enums and a struct. (Composite Attribute) The new version adds a few additional enums.

For some reason the migration is successfully executed on my iPad, but skipped in Simulators and on my iPhone. Despite installing the same versions from Xcode or Testflight.

When the migration is skipped the newly added enums are not initialised (despite default values) and results in the following error:

CoreData: warning: validation recovery attempt FAILED with Error Domain=NSCocoaErrorDomain Code=1560 "Multiple validation errors occurred." UserInfo={NSDetailedErrors=(
    "Error Domain=NSCocoaErrorDomain Code=1570 \"%{PROPERTY}@ is a required value.\"

I have already checked the versionChecksums of the DataModels, and they are always matching for V1. With breakpoints I have also checked that on all devices & simulators the MigrationPlan is loaded, but only run on iPad. Tested it on iOS 17.4 and 17.5.

MigrationPlan

enum DataMigrationPlan: SchemaMigrationPlan {
    
    static var schemas: [any VersionedSchema.Type] {
        [DataSchemaV1.self, DataSchemaV2.self]
    }
        
    static let migrateV1toV2 = MigrationStage.custom(
        fromVersion: DataSchemaV1.self,
        toVersion: DataSchemaV2.self,
        willMigrate: { context in
            print("will migrate")
        }, didMigrate: { context in
            print("did migrate")
            // … setting values for new properties
        }
    )
    
    static var stages: [MigrationStage] {
        [migrateV1toV2]
    }
}

ModelContainer

let modelConfiguration = ModelConfiguration(isStoredInMemoryOnly: false)
sharedModelContainer = try ModelContainer (
    for: DataSchemaV2.FilterGroup.self,
    migrationPlan: DataMigrationPlan.self,
    configurations: modelConfiguration
)

DataSchemaV1

enum DataSchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    
    static var models: [any PersistentModel.Type] {
        [DataSchemaV1.FilterGroup.self]
    }

    @Model
    final class FilterGroup: Identifiable {
        public let id: String = UUID().uuidString
        var timestamp: Date = Date.now

        var theme: WidgetTheme = WidgetTheme.calendar // string enum
        var myStruct: [MyStruct] = []
    }

    static var sampleData: FilterGroup {
        return FilterGroup(...)
    }

    struct MyStruct: Codable, Identifiable {
        var id: UUID = UUID()
        var someEnum: SomeEnum = SomeEnum.something
        ...
    }
}

DataSchemaV2

enum DataSchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)
    
    static var models: [any PersistentModel.Type] {
        [DataSchemaV2.FilterGroup.self]
    }

    @Model
    final class FilterGroup: Identifiable {
        // ... same as v1
        var newlyAddedEnum: NewEnum = NewEnum.something
    }

    // ...
}

Any ideas why the migration is only triggered on some devices?

Upvotes: 1

Views: 240

Answers (1)

ARS
ARS

Reputation: 387

It turns out the problem was that I also created a ModelContainer in my Widget. There I did not add the MigrationPlan. This might have lead to a race condition where in some cases the ModelContainer was first migrated by my main app using the customMigrationPlan, and in some cases by the Widget using a lightweight migration.

After adding the MigrationPlan in the Widget, the migration is run as expected.

How to debug

If you are in a similar situation this helped me finally debug it:

  1. Create you ModelContainer and then get the url using this code
let url = myModelContainer.mainContext.configurations.first?.url.path(percentEncoded: false)
  1. Open it with a tool like DB Browser for SQLite. (or you can use sql in the terminal as explained here: How to read SwiftData store)
  2. Then inspect the ATransactionString table and you will see all the migrations that took place. In my case I found a "com.apple.coredata.schemamigrator: lightweight migration ..." and my Widget target listed.

Upvotes: 1

Related Questions