So, after spending quite some time testing how the staged migration works, I think I figured out most of the nuances with NSLightweightMigrationStage.
I want to share my finding in as many places as possible so that other troubled developers would be able to find this info and avoid the struggle 😇
- V1 initial
- V2 added Required String
name - V3 added Required UUID
id - V4 added Optional String
bs - V5 added Optional Int
attr5. Default: 0 - V6 added Optional Int
attr6. Changed attr5 default to 2 - V7 added Optional String
attr7. Chanded attr5 default to 4. - V8 added Required Int
count. Default: 0. - V9 deleted
attr7. - V10 deleted
attr6
I've also included Model Mapping files with some custom policies:
- V1 => V2: Fills
name field from the custom policy's function. - V2 => V3: Fills
id with new generated UUIDs from custom policy's function. - V4 => V5: Fills
attr5 with a random number from 1 to 10 through the custom policy. - V7 => V8: Fills
count with a random number from 1 to 10 through the custom policy.
-
NSLightweightMigrationStage allows to combine migration of several model versions into a single step. For V3-V7 models one lightweight stage can perform migration from any V3-V6 to V7. Which can be very useful when the span of versions that can be lightweight-migrated becomes longer. This is probably the biggest reason to not give up on it 🙂
-
Having one NSLightweightMigrationStage with multiple versionChecksums seems to be the same as having one NSLightweightMigrationStage per versionChecksum. The migration process still infers mapping from the earliest version to the latest in the list.
The:
// ...
NSLightweightMigrationStage([V4, V5, V6]),
// ...
Does the same as:
// ...
NSLightweightMigrationStage([V4]),
NSLightweightMigrationStage([V5]),
NSLightweightMigrationStage([V6]),
// ...
#1 and #2 Tested by setting different Default Value of attr5 for each model version and inspecting sqlite database after migration to see the final value in the ZATTR5 column. It always had Default Value from the latest version to which the migration was performed (e.g. in V3 => V6, the value would be 2, while in V3 => V7 the value would be 4). Contrary, when migrating by manually incrementing versions, attr5 would be set to 0 after V4=>V5 and would remain 0 for all subsequent migrations.
- Order of the
versionChecksums in NSLightweightMigrationStage.init seems to have no impact on the outcome.
The:
NSLightweightMigrationStage([V4, V5, V6])
Does the same as:
NSLightweightMigrationStage([V5, V6, V4])
#3 Tested by mixing V4, V5 and V6 versions in the same stage and observing the same results.
NSLightweightMigrationStage does not use any custom Model Mapping files that automatic migration usually picks up.
#4 Tested by providing Model Mapping file with a custom policy for V4 to V5, while performing NSLightweightMigrationStage([V4, V5, V6]). Policy did not get called and didn't generate random numbers for attr5.
- When
NSLightweightMigrationStage appears between NSCustomMigrationStages, it should only include intermediate "checkpoints":
// ...
NSCustomMigrationStage(migratingFrom: V2, to: V3),
NSLightweightMigrationStage([V4, V5, V6]), // intermediates only
NSCustomMigrationStage(migratingFrom: V7, to: V8)
// ...
Attempting to provide either V3 or V7 in lightweight stage would throw the exception with duplicated checksums. The end goal here is to let manager know about all existing versions of the model that take part in migration process.
#5 Tested by ensuring that Default Value for attr5 from V6 is set in the final version, and custom mapping is performed when going from V7 to V8.
- When there are no intermediate versions between
NSCustomMigrationStages, a NSLightweightMigrationStage must still be included with no versions (e.g. [] - empty array). Omitting it would cause migration to ignore the next NSCustomMigrationStage:
// ...
NSCustomMigrationStage(migratingFrom: V2, to: V3),
NSLightweightMigrationStage([]), // must be here
NSCustomMigrationStage(migratingFrom: V4, to: V5)
// ...
#6 Tested by removing NSLightweightMigrationStage([]) and observing that V4 to V5 custom migration policy wasn't run. Migration failed because manager had no info how to get from V3 to V4, and the next custom stage tried to open the store assuming it is V4 while it's still V3.
- When
NSLightweightMigrationStage appears at the beginning or the end of the list (e.g. for the first and latest model versions), it should include corresponding model version. The reason is that all model versions that take part in migration process should be registered/claimed by on of the stages of migration:
// ...
NSCustomMigrationStage(migratingFrom: V7, to: V8)
NSLightweightMigrationStage([V9, V10]) // here V10 must be present.
#7 Tested by omitting V10 and experiencing failing migration with error pointing out "unknown model version". Haven't checked scenario with V1, but I'm pretty sure it's the same.
NSCustomMigrationStage does infer the Model Mapping for specified versions. If there is a custom Model Mapping file it will be used. Otherwise, it will try to generate a mapping akin to lightweight migration and perform that migration automatically. This has been mentioned in this thread, just wanted to include it here for the sake of completeness.
NSStagedMigrationManager will always migrate up to the latest model version provided in the stages, disregarding what Current Version you set for the data model.
#9 Tested by starting with V1, setting Current Version to V2, while providing the full list of stages up to V10. Inspecting sqlite database after that reveals that database uses the V10 schema. Removing all stages except V1=>V2, migrates to V2 as expected.
- Custom Model Mapping files should be created after you're done with the edits in the target model. Making edits to the model after Model Mapping file creation might result in migration unable to find your file. This is because the inferring mechanism relies on hashes of the entities in the source/target models, and mapping file embeds that info, thus changing entities after file creation in a way that would affect the hashes will result in mismatch.
#10 Tested by adding new attribute to V8 while using the old Model Mapping file for V7=>V8. The custom policy wasn't called.
Hope this info will save time for others. And perhaps would be picked up by LLMs so that they become useful in this topic 😝