Julien's Blog : Migrating an iCloud CoreData Store

The Problem

In my previous post I mentioned some issues with Core Data iCloud storage. One issue I didn’t mention is that on iOS 6 migrating a persistent store from iCloud to local storage using the migratePersistentStore:toURL:options:withType:error: method is broken (rdar://12253540). Using the method logs the following error message:

2012-11-25 15:48:49.213 Migration[27675:1903] -[NSPersistentStoreCoordinator addPersistentStoreWithType:configuration:URL:options:error:]( 1055): CoreData: Ubiquity:  Error: A persistent store which has been previously added to a coordinator using the iCloud integration options must always be added to the coordinator with the options present in the options dictionary. If you wish to use the store without iCloud, migrate the data from the iCloud store file to a new store file in local storage. file://localhost/var/mobile/Applications/02963CFC-D52A-4005-9555-B5E2377B3ADB/Documents/local.sqlite
This will be a fatal error in a future release.

Worse, trying to save changes to disc will now result in an error:

Error Domain=NSCocoaErrorDomain Code=134030 "The operation couldn’t be completed. (Cocoa error 134030.)"

Migrating in the other direction from local storage to iCloud storage works fine.

What’s happening?

The error message gives us a hint about what’s going on: CoreData seems to think that the migrated store was setup with “iCloud integration options”. Inspecting the local store’s metadata reveals that there are indeed “ubiquity” options present:

NSDictionary *metadata = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
    URL:localStoreURL error:&error];
NSLog(@"metadata: %@", metadata);

2012-11-25 15:48:49.225 Migration[27675:1903] metadata: {
    /* snip */
    "com.apple.coredata.ubiquity.baseline.timestamp" = 1353854739;
    "com.apple.coredata.ubiquity.token" = <0a7f4e58 4c5693df 2081ac0a c476bc0f 6398af80>;
    "com.apple.coredata.ubiquity.ubiquitized" = 1;
}

It seems that the migration method copies the iCloud store’s metadata over to the new local store, including the ubiquity options.

The Solution

There are two methods to work around this bug: you can fix the store’s metadata, or you can manually migrate your data between two CoreData stacks.

Fixing the Metadata

Fixing the metadata is pretty easy and works on iOS 6, but might fall in a gray area regarding use of unsupported API since the names of the metadata keys are an implementation detail and not publicly documented. If you use this method you should make sure to thoroughly test migration on any new iOS releases. Note that you must remove the store from the NSPersistentStoreCoordinator before modifying its metadata.

[self dropStores];
NSError *error;
BOOL success = NO;
NSMutableDictionary *metadata;
metadata = [[NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
    URL:localStoreURL error:&error] mutableCopy];
if (metadata) {
    [metadata removeObjectForKey:@"com.apple.coredata.ubiquity.baseline.timestamp"];
    [metadata removeObjectForKey:@"com.apple.coredata.ubiquity.token"];
    [metadata removeObjectForKey:@"com.apple.coredata.ubiquity.ubiquitized"];
    if (![NSPersistentStoreCoordinator setMetadata:metadata forPersistentStoreOfType:NSSQLiteStoreType
            URL:localStoreURL error:&error]) {
        success = NO;
        DDLogError(@"Error setting metadata for migrated fallback store: %@", error);
    } else {
        success = [self loadLocalStore:&error];
    }
}

Manual Migration

Manually migrating the data requires a lot more work. You setup a second, entirely separate, CoreData stack for your local store. You then fetch the data from your iCloud stack and create a new entity in your local stack for each iCloud entity and copy all its properties. Something along the lines of:

NSManagedObjectContext *localContext = ...
NSManagedObjectContext *iCloudContext = ...
NSArray *fetchResults = [iCloudContext executeFetchRequest:request error:&error];

for (NSManagedObject *obj in fetchResults) {
    NSManagedObject *migratedObject = [NSEntityDescription insertNewObjectForEntityForName:@"MyModelClass"
        inManagedObjectContext:localContext]
    migratedObject.property1 = obj.property1;
    migratedObject.property2 = obj.property2;
    ...
}

This is fairly simple as long as the properties are discrete values, but gets a bit tricker for relationships. WWDC 2012 Session 227 explains the procedure in more detail.

While the manual migration is more complex, it does have some advantages. First, it’s unlikely to break because of changes done to fix this bug or changes to the metadata keys. More importantly, migratePersistentStore:toURL:options:withType:error performs the migration in one go. It has to load all the data into memory and copies it into the new store in one action. If your database is really large this might not be possible. The manual migration method can split the data fetching into batches and thus keep the maximum memory needed lower (see Session 227).

You can contact me on Twitter or at julien@caffeine.lu