Reputation: 6166
Doing light migration, using default value, rather than optional value for new param does not seems to work and keeps crashing.
I recall an Apple SwiftData migration guide, but it seems to be gone, so I could not find any mention regarding this.
Initial version
@Model
final class Car {
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}
Updated version (using default value)
@Model
final class Car {
var timestamp: Date
var engine: Engine = Engine()
init(timestamp: Date, engine: Engine) {
self.timestamp = timestamp
self.engine = engine
}
}
@Model
class Engine {
var horsepower: Int = 0
init(horsepower: Int) {
self.horsepower = horsepower
}
init() {
self.horsepower = 0
}
}
This crashes: entity=Car, attribute=engine, reason=Validation error missing attribute values on mandatory destination relationship
Using optional
@Model
final class Car {
var timestamp: Date
var engine: Engine? // <<< Using optional for new property
init(timestamp: Date) {
self.timestamp = timestamp
}
}
This one works.
Here's the migration plan
enum DASchemeV1: VersionedSchema {
static var versionIdentifier: Schema.Version {
Schema.Version(1, 0, 0)
}
static var models: [any PersistentModel.Type] {
[Car.self]
}
@Model
final class Car {
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}
}
enum DASchemeV2: VersionedSchema {
static var versionIdentifier: Schema.Version {
Schema.Version(1, 0, 1)
}
static var models: [any PersistentModel.Type] {
[
Car.self,
Engine.self
]
}
@Model
final class Car {
var timestamp: Date
var engine: Engine = Engine()
init(timestamp: Date, engine: Engine) {
self.timestamp = timestamp
self.engine = engine
}
}
@Model
class Engine {
var horsepower: Int = 0
init(horsepower: Int) {
self.horsepower = horsepower
}
init() {
self.horsepower = 0
}
}
}
struct ItemMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[DASchemeV1.self, DASchemeV2.self]
}
static var stages: [MigrationStage] {
[migrateV1toV2]
}
static let migrateV1toV2 = MigrationStage.lightweight(
fromVersion: DASchemeV1.self,
toVersion: DASchemeV2.self
)
}
Does this means that only optional properties can be added in migration? Or that using a default value needs to do a custom migration?
Some information about custom migration as I understand it:
static let migrationV2ToV2 = MigrationStage.custom(
fromVersion: SchemeV1.self,
toVersion: SchemeV2.self,
willMigrate: { context in
// Context here can read old store: SchemeV1
let items = try context.fetch(FetchDescriptor<SchemeV1.Item>())
}, didMigrate: { context in
// Context here can read the new store: SchemeV2
let items = try context.fetch(FetchDescriptor<SchemeV2.Item>())
})
Is there a simple example for a migration (light or custom) for adding a non primitive non optional property?
From
@Model
final class User {
var name: String
init(name: String) {
self.name = name
}
}
To
@Model
final class Company {
var name: String
init(name: String) {
self.name = name
}
}
@Model
final class User {
var name: String
var company: Company = Company(name: "Example")
init(name: String, company: Company) {
self.name = name
self.company = company
}
}
Upvotes: 0
Views: 68
Reputation: 382
You can add non optional attributes to a SwiftData model in a migration, but to have a default value you must use a custom migration:
static let migrateV2toV3: MigrationStage = .custom(
fromVersion: DataSchemaV1.self,
toVersion: DataSchemaV2.self,
willMigrate: nil
) { context in // didMigrate
let cars = try context.fetch(FetchDescriptor<DataSchemaV2.Car>())
cars.forEach { $0.engine = Engine() }
try context.save()
}
}
Unfortunately, adding properties with default values require a custom migration where you set that value. It's not smart enough to both add the new field and populate it with a custom value in light migrations.
Upvotes: 1