How do Core Data migrations work: lightweight, staged and heavyweight, and what about CloudKit?
Predict first
Can Core Data infer a mapping for adding an optional attribute?
import CoreData
func makeModel(withNickname: Bool) -> NSManagedObjectModel {
let m = NSManagedObjectModel()
let e = NSEntityDescription()
e.name = "Person"
e.managedObjectClassName = "NSManagedObject"
let n = NSAttributeDescription()
n.name = "name"
n.attributeType = .stringAttributeType
n.isOptional = false
var props = [n]
if withNickname {
let k = NSAttributeDescription()
k.name = "nick"
k.attributeType = .stringAttributeType
k.isOptional = true
props.append(k)
}
e.properties = props
m.entities = [e]
return m
}
let inferred = try? NSMappingModel.inferredMappingModel(forSourceModel: makeModel(withNickname: false), destinationModel: makeModel(withNickname: true))
print(inferred != nil)
- true, a lightweight migration is possible
- false, an explicit mapping model is required
Mental model. A migration is moving house: for small changes (an extra shelf) the movers can work it out themselves (lightweight); for rebuilding rooms you need a floor plan (a mapping model); and a rented warehouse (CloudKit) only lets you add shelves, never remove them.
When you ship a new version of the model, an existing store must be migrated to match, or Core Data refuses to open it. Edit a model by adding a new model version (.xcdatamodel inside the .xcdatamodeld) and mark it current; never edit a shipped version. Lightweight migration is the default and should cover most changes: Core Data infers the mapping for adding an attribute (optional or with a default), removing one, renaming with a renaming identifier on the property, adding or removing entities and relationships, and changing optionality. NSPersistentContainer turns it on by default (shouldMigrateStoreAutomatically and shouldInferMappingModelAutomatically), and you can test that an inference is possible with NSMappingModel.inferredMappingModel(forSourceModel:destinationModel:), which returns a mapping when adding an optional attribute. Heavyweight migration uses an explicit mapping model (and optionally custom NSEntityMigrationPolicy subclasses) for transformations Core Data cannot infer: splitting or merging entities, computing new values, changing types. Across several versions, migrate step by step (v1→v2→v3) rather than writing one huge mapping; recent SDKs add staged migration (NSStagedMigrationManager) to combine lightweight and custom stages in order.
Practical rules: test every upgrade path with real stores from each shipped version (not just the previous one), keep the migration inside the app launch but off the main thread where possible (large stores can take seconds, and a slow launch gets the app killed), back up the store before migrating, and plan a fallback (never silently delete a user’s store on failure). With CloudKit (NSPersistentCloudKitContainer) the model also feeds the CloudKit schema, which adds constraints: attributes need defaults or be optional, unique constraints are not supported, relationships must be optional with inverses, and the production CloudKit schema is additive only (you can add fields and record types but never delete or rename them), so a model change is effectively permanent for synced data. Develop against the development environment, deploy the schema to production before shipping, and treat renames as add-new-and-copy. The same version-and-migrate discipline applies to SQLite (user_version) and to SwiftData’s VersionedSchema.
Adding an optional attribute is the textbook lightweight change, so Core Data can infer the mapping itself.
Common traps
- Editing a shipped model version. Existing stores no longer match any known version and fail to open; add a new version.
- Testing only the previous version’s upgrade. Users upgrade from any old version; test every path.
They might follow up with
- What makes a change lightweight? Core Data can infer the mapping: adding optional attributes, renames with an identifier, adding entities.
- Why is a CloudKit schema change risky? The production schema can only grow, so mistakes cannot be undone.
Related questions
- 13.3.2 How do you use Core Data across threads: contexts, perform, merging changes and merge policies?
- 13.2.2 How do you use SQLite safely and quickly: bound parameters, indexes, and schema migrations?
- 13.4.2 How do ModelActor, context-per-actor and VersionedSchema work in SwiftData, and what can you pass between…
← How do you use Core Data across threads: contexts, perform, merging…