Search code examples
swiftswiftdata

SwiftData Migration Error: Missing Attribute Values on Mandatory Relationship


I'm currently working on a data model migration in Swift using a custom schema migration plan.

My project involves migrating settings from one schema version to another (SchemaV1 to SchemaV2). Both schema versions include a class named FSViz, but with slightly different properties.

In the newer schema, SchemaV2, I introduced a new property named textSettings of type TextSetting, replacing the textColor property from SchemaV1.

Here's a simplified version of my migration code and class definitions:

Model:

extension SchemaV1 {
    @Model
    public final class FSViz {
        @Attribute(.unique) public let id: UUID
        public var textColor: TextColor

        init(textColor: TextColor) {
            self.id = UUID()
            self.textColor = textColor
        }
    }
}

extension SchemaV2 {
    @Model
    public final class FSViz {
        @Attribute(.unique) public let id: UUID
        public var textSettings: TextSetting

        init(textSettings: TextSetting) {
            self.id = UUID()
            self.textSettings = textSettings
        }
    }
}

initMyApp:

public struct InitMyApp {
    static func makeInitialFSViz() -> FSViz? {
        FSViz(textSettings: makeTextSettings())
    }

    public static func makeFSViz() -> FSViz {
        FSViz(textSettings: makeTextSettings())
    }

    static func makeTextSettings() -> TextSetting {
        TextSetting(
                     textColor: TextColor(red: 1, green: 1, blue: 1, alpha: 1)
        )
    }
}

MigrationPlan:

enum MyAppMigrationPlan: SchemaMigrationPlan {
    static var schemas: [VersionedSchema.Type] {
        [SchemaV1.self, SchemaV2.self]
    }

    static var stages: [MigrationStage] {
        [migrateV1toV2]
    }

    static let migrateV1toV2 = MigrationStage.custom(fromVersion: SchemaV1.self, toVersion: SchemaV2.self, willMigrate: { context in
        let fetchDescriptor = FetchDescriptor<SchemaV1.FSViz>()
        let allV1Vizs = try context.fetch(fetchDescriptor)

        for v1Viz in allV1Vizs {
            let newTextSetting = SchemaV2.TextSetting(textColor: v1Viz.textColor)
            let newViz = SchemaV2.FSViz(textSettings: newTextSetting)

           print("migration processing")
            context.insert(newViz)
            try context.save()
        }
    }, didMigrate: nil)
}

MyAppContainer:

public typealias FSViz = SchemaV2.FSViz
public typealias TextSetting = SchemaV2.TextSetting

@MainActor
public let MyAppContainer: ModelContainer = {
    do {
        let schema = Schema([FSVis.self, TextSetting.self])
        let configuration = ModelConfiguration()
        let container = try ModelContainer(for: schema,
                                           migrationPlan: MyAppMigrationPlan.self)
        let context = container.mainContext
        if try context.fetch(FetchDescriptor<FSViz>()).isEmpty {
            if let fsViz = InitWaveBar.makeInitialFSViz() {
                container.mainContext.insert(fsViz)
            }
            else {
                print("Error: makeInitialFSViz() returned nil")
            }
        }
        return container
    }
    catch {
        fatalError(error.localizedDescription)
    }
}()

However, when running the migration, I encounter the following error:

Error Domain=NSCocoaErrorDomain Code=134110 "An error occurred during persistent store migration."
UserInfo={entity=FSViz, attribute=textSettings, reason=Validation error missing attribute values on mandatory destination relationship}

This error suggests that there are missing attribute values on the mandatory textSettings relationship in the destination schema. I've double-checked my migration logic to ensure that textSettings is correctly initialized and assigned, but the error persists.

Questions:

How can I resolve the "missing attribute values on mandatory destination relationship" error during the migration? Is there a specific aspect of handling relationships during migrations in Swift that I'm overlooking?

Any insights or suggestions on how to troubleshoot and resolve this migration issue would be greatly appreciated.


Solution

  • The core of the problem was not properly handling the textSettings property during migration.

    To solve this, I provide the default value TextSetting (newly added in SchemaV2) in the model.

    Instead of using optional parameter, provide default value to the model recommended.

    extension SchemaV1 {
        @Model
        public final class FSViz {
            @Attribute(.unique) public let id: UUID
            public var textColor: TextColor
    
            init(textColor: TextColor) {
                self.id = UUID()
                self.textColor = textColor
            }
        }
    }
    
    extension SchemaV2 {
        @Model
        public final class FSViz {
            @Attribute(.unique) public let id: UUID
            // provide default value recommended
            public var textSettings: TextSetting = TextSetting(text: "") 
            // if it is optional
            // public var textSettings: TextSetting? 
    
            init(textSettings: TextSetting) {
                self.id = UUID()
                self.textSettings = textSettings
            }
        }
    }
    

    Rather than go into more detail, I hope you might this answer helpful.