SwiftData unversioned migration

Hi,

I'm struggling with SwiftData and the components for migration and could really use some guidance. My specific questions are

  • Is it possible to go from an unversioned schema to a versioned schema?
  • Do all @Model classes need to be converted?
  • Is there one VersionedSchema for the entire app that handles all models or one VersionedSchema per model?
  • What is the relationship, if any, between the models given to ModelContainer in a [Schema] and the models in the VersionedSchema in a [any PersistentModel.Type]

I have an app in the AppStore. I use SwiftData and have four @Models defined. I was not aware of VersionedSchema when I started, so they are unversioned. I want to update the model and am trying to convert to a VersionedSchema. I've tried various things and can't even get into the migration plan yet. All posts and tutorials that I've come across only deal with one Model, and create a VersionedSchema for that model.

I've tried to switch the one Model I want to update, as well as switching them all. Of course I get different errors depending on what configuration I try.

It seems like I should have one VersionedSchema for the app since there is the static var models: [any PersistentModel.Type] property. Yet the tutorials I've seen create a TypeNameSchemaV1 to go with the @Model TypeName.

Which is correct? An AppNameSchemaV1 which defines four models, or four TypeNameSchemaV1?

Any help will be much appreciated

Answered by DTS Engineer in 799813022

First, I'd like to point you to the following post, where I provided an example that demonstrates how to wrap SwiftData models with a versioned schema (VersionedSchema).

Based on that example, we can look into your questions:

Is it possible to go from an unversioned schema to a versioned schema?

Yes. Assuming you have a model named Item at the beginning (version 1):

@Model
final class Item {
    var timestamp: Date = Date.now
    
    init(timestamp: Date = .now) {
        self.timestamp = timestamp
    }
}

To evolve to version 2, you wrap Item with a versioned schema in your version 2 app:

enum ItemSchemaV1: VersionedSchema {
    static var versionIdentifier: Schema.Version {
        return Schema.Version(1, 0, 0) //"ItemSchemaV1"
    }
    
    static var models: [any PersistentModel.Type] {
        [Item.self]
    }

    @Model
    final class Item {
        var timestamp: Date = Date.now
        
        init(timestamp: Date = .now) {
            self.timestamp = timestamp
        }
    }
}

From there, you can create your version 2 schema and migration plan (SchemaMigrationPlan), as shown in the mentioned example. When SwiftData runs the migration plan, it maps Items in the version 1 store, to ItemSchemaV1.Items (because their version hashes are the same, I believe), and migrates the store to version 2.

Do all @Model classes need to be converted?

I am not quite clear what the real question here – If you are asking whether you need to put all the models in a versioned schema, the answer is yes.

Is there one VersionedSchema for the entire app that handles all models or one VersionedSchema per model?

It should be one versioned schema for each version. In the mentioned example, Item is wrapped in ItemSchemaV1, and both Item2 and the new version Item are wrapped in ItemSchemaV2.

What is the relationship, if any, between the models given to ModelContainer in a [Schema] and the models in the VersionedSchema in a [any PersistentModel.Type]

The schema used to create a model container should be the current version schema, in this case, version 2 schema.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Accepted Answer

First, I'd like to point you to the following post, where I provided an example that demonstrates how to wrap SwiftData models with a versioned schema (VersionedSchema).

Based on that example, we can look into your questions:

Is it possible to go from an unversioned schema to a versioned schema?

Yes. Assuming you have a model named Item at the beginning (version 1):

@Model
final class Item {
    var timestamp: Date = Date.now
    
    init(timestamp: Date = .now) {
        self.timestamp = timestamp
    }
}

To evolve to version 2, you wrap Item with a versioned schema in your version 2 app:

enum ItemSchemaV1: VersionedSchema {
    static var versionIdentifier: Schema.Version {
        return Schema.Version(1, 0, 0) //"ItemSchemaV1"
    }
    
    static var models: [any PersistentModel.Type] {
        [Item.self]
    }

    @Model
    final class Item {
        var timestamp: Date = Date.now
        
        init(timestamp: Date = .now) {
            self.timestamp = timestamp
        }
    }
}

From there, you can create your version 2 schema and migration plan (SchemaMigrationPlan), as shown in the mentioned example. When SwiftData runs the migration plan, it maps Items in the version 1 store, to ItemSchemaV1.Items (because their version hashes are the same, I believe), and migrates the store to version 2.

Do all @Model classes need to be converted?

I am not quite clear what the real question here – If you are asking whether you need to put all the models in a versioned schema, the answer is yes.

Is there one VersionedSchema for the entire app that handles all models or one VersionedSchema per model?

It should be one versioned schema for each version. In the mentioned example, Item is wrapped in ItemSchemaV1, and both Item2 and the new version Item are wrapped in ItemSchemaV2.

What is the relationship, if any, between the models given to ModelContainer in a [Schema] and the models in the VersionedSchema in a [any PersistentModel.Type]

The schema used to create a model container should be the current version schema, in this case, version 2 schema.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Thank you for the reply, I'll take a look at the link you posted, but wanted to clarify my questions.

I currently have four models: Item, Thing, Element, Piece creating the model container like so:

var sharedModelContainer: ModelContainer = {
        let schema = Schema([
            Item.self,
            Thing.self,
            Element.self,
            Piece.self
        ])
        let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)

        do {
            return try ModelContainer(for: schema, configurations: [modelConfiguration])
        } catch {
            fatalError("Could not create ModelContainer: \(error)")
        }
    }()

Do all @Model classes need to be converted?

I plan on updating all models, but I was really asking what is necessary. And it ties into the following question

Is there one VersionedSchema for the entire app that handles all models or one VersionedSchema per model?

So I'm asking, do I create MyAppSchemaV1 and define the models there, or do I need to create ItemSchemaV1, ThingSchemaV1, ElementSchemaV1, and PieceSchemaV1 - then I would add ItemSchemaV2?

Finally you state

The schema used to create a model container should be the current version schema, in this case, version 2 schema.

This leads me to believe I should have a single schema for the entire app. Am I understanding things correctly so far?

You create MyAppSchemaV1 and put all four model classes in there. That is version 1 schema. You then create MyAppSchemaV2 and put all four model classes, which are updated as needed, in there as well. That is version 2 schema. You use version 2 schema to create your model container in your app.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Ok Great!

I have created my VersionedSchemas that define my four models and included them in the models property. And created my ModelContainer using the schema initializer Schema(versionedSchema: SchemaV2.self)

Yet nothing works. I'm getting a few errors including:

error: Attempting to retrieve an NSManagedObjectModel version checksum while the model is still editable. This may result in an unstable verison checksum. Add model to NSPersistentStoreCoordinator and try again.

and

error: Store failed to load.  <NSPersistentStoreDescription: 0x600003a85cb0> (type: SQLite, url: <redacted>
with error = Error Domain=NSCocoaErrorDomain Code=134504 "Cannot use staged migration with an unknown model version." UserInfo={NSLocalizedDescription=Cannot use staged migration with an unknown model version.} with userInfo {
    NSLocalizedDescription = "Cannot use staged migration with an unknown model version.";
}

I’m concerned that I may have corrupted my database with all the things I’ve been trying. Is there a chance that the database is corrupted, or is there something fundamentally wrong in my setup that could be causing these issues?

The first error can be ignored, as it doesn't seem to have any harm on your data.

The second one seems strange. If you can provide a minima project that contains only the code relevant to the issue, with detailed steps to reproduce the issue. I may be able to take a closer look.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

I'm attempting to use my original code on a clean simulator to generate some data then apply the migration code to see if I get the same errors of if it now works properly.

If I still have issues, I will build a custom project to replicate. Until then, I've accepted the answer.

Thank you for the help

Just wanted to update for anyone ending up here in the future. Making good progress with this technique after starting fresh.

I must have corrupted my original database with half migrations from the many things I was trying out.

Hi Ziqiao!

I continue to get the error error: NSLocalizedDescription : Cannot use staged migration with an unknown model version.

I thought everything was coming together, but I was mistakenly testing a migration from V1 to V2 instead of testing un-versioned migrating to V2.

I thought I read SwiftData uses a checksum to tell if something is the same. Is that true? If so, do we know how it calculates that value?

If my @Model defines functions and computed properties, are those used in the checksum? Can those be moved to an extension to reduce code duplication or am I now required to carry those functions and properties along for every version I create?

If I have RandomFile.swift and I define

@Model
final class Item {
        var id: UUID = UUID()
        var name: String
        var isDefault: Bool

        init(name: String, isDefault: Bool = false) {
            self.name = name.localizedCapitalized
            self.isDefault = isDefault
        }

        private func doSomething(_ val: Int) { }
        
         var compProp: String {
             return "x"
        }
    }

Then I create SchemaV1.swift with extracted @Models

enum SchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] {
        [Item.self, Thing.self, Element.self, Piece.self]
    }

    @Model
    final class Item {
        var id: UUID = UUID()
        var name: String
        var isDefault: Bool

        init(name: String, isDefault: Bool = false) {
            self.name = name.localizedCapitalized
            self.isDefault = isDefault
        }
    }
   @Model final class Thing { ... }
   @Model final class Element { ... }
   @Model final class Piece { ... }
}

And change RandomFile.swift to include

extension Item {
   private func doSomething(_ val: Int) { }
        
         var compProp: String {
             return "x"
        }
}

Do I need to define all this extra stuff in my SchemaVx.swift file for every model? And again for every Vx I create?

@DTS Engineer Hope you have a nice holiday weekend and we can get back to this next week

I continue to get the error error: NSLocalizedDescription : Cannot use staged migration with an unknown model version.

This doesn't happen to me. If you can provide a minimal project that contains only the code relevant to the issue, with detailed steps to reproduce the issue. I may be able to take a look.

And change RandomFile.swift to include

Putting Item in SchemaV1 makes the item type SchemaV1.Item, and so extension Item doesn't apply to SchemaV1.Item.

What I normally do is to define a custom compile directive, and use typealias to make Item the alias of SchemaV1.Item, as shown below:

#if SCHEMA_V1 
typealias Item = SchemaV1.Item
#endif

Here, SCHEMA_V1 is defined for your version 1, which is shown in the attached screenshot.

This way, the code based on Item in your original version is now based on SchemaV1.Item in your version 1

Best,
——
Ziqiao Chen.
 Worldwide Developer Relations.

All of the models are type aliased, so the extension will apply to any version of the model. Which is the whole point to avoid duplicating the functions and properties every time I create a new schema version

Here is my question again

If my @Model defines functions and computed properties, are those used in the checksum? Can those be moved to an extension to reduce code duplication or am I now required to carry those functions and properties along for every version I create?

I have two branches in my repository that can be used to replicate, but I will need your GitHub username to give you access

@jmilillo

I had the same issue with "Cannot use staged migration with an unknown model version."

Do you get this on many devices/simulators? Try delete the app from the simulator and try again

I'm having the same "Cannot use staged migration with an unknown model version." error for my app on production.

I started with an unversioned SwiftData model with iCloud, then released a new version for my app that puts the same model inside of V1 and also have a lightweight migration to V2. Now, all the users are complaining about the crash on launch. I spent a day to find a solution for the issue but unfortunately the only solution is deleting the app and installing it again.

Looks like Apple is not willing to admit this issue and fix it. Therefore, I highly suggest people who use SwiftData to start with versioned schema. Or if you already shipped an unversioned schema, first put your model into a versioned schema and release a new build for your app. Wait for most of your users to update to that build. Then, create another release that includes your migration. These are the only ways to avoid the crash on launch. It is good that I used iCloud so nobody lost their data.

SwiftData unversioned migration
 
 
Q