Mostly Undocumented

Last year I wrote about backing up and restoring Core Data. Recently Arnaud Joubay messaged me to ask about it. I used a method called migratePersistentStore(...) to duplicate a persistent store. Arnaud asked why I had not used a similar method called replacePersistentStore(...) instead. He also sent me a link to a post on Apple’s dev forum site, attributed to an anonymous framework engineer, which had this to say on the topic:

Additionally you should almost never use NSPersistentStoreCoordinator’s migratePersistentStore… method but instead use the newer replacePersistentStoreAtURL.. (you can replace emptiness to make a copy). The former loads the store into memory so you can do fairly radical things like write it out as a different store type. It pre-dates iOS. The latter will perform an APFS clone where possible.

There are two-ish answers to Arnaud’s question. The first is that I didn’t think of it, which isn’t a great answer. The second is that this method is almost totally undocumented, so you’re on your own working out how to use it. The dev forums post mentioned above is from summer 2020. The replacePersistentStore(...) method was introduced five years earlier in iOS 9, but the forum post was the first time most of the information appeared.

The second-and-a-half reason is that this is the first suggestion I’ve seen that migratePersistentStore(...) might not be a good idea anymore. It’s not deprecated and I haven’t seen any previous source recommending against its use. Even if I had thought of using the newer method, I still might not have used it because there wasn’t any obvious reason to choose one over the other.

So I did some digging to see what I could find out. Hopefully the steps I took will help others in similar situations.

Your function is only mostly undocumented

Full disclosure, there is, technically, documentation. Here’s the entire entry from Apple’s developer docs:

Replace the destination persistent store with the source store.

I don’t count that as documented because you could figure out that much from the function declaration, which is also included:

func replacePersistentStore(at destinationURL: URL, 
                           destinationOptions: [AnyHashable : Any]? = nil, 
            withPersistentStoreFrom sourceURL: URL, 
                                sourceOptions: [AnyHashable : Any]? = nil, 
                             ofType storeType: String) throws

But at least it’s not completely undocumented, so you can use it without getting blocked from the app store. I’ve reported this as FB9054409, but let’s see if I can figure out anything on my own.

The Ghosts of WWDC Past

I looked up WWDC 2015, when the method was introduced, to see if I could learn anything more. Sure enough it was mentioned in “What’s New in Core Data”. From watching the session and looking at the slides I learned that the method

  • Creates a copy of the persistent store if the destination doesn’t already exist.
  • Has the same pattern as destroyPersistentStoreAtURL, which was also introduced that year.

I wasn’t sure what the “same pattern” meant, but the previous slide says that the destroyPersistentStore(...) method

  • Honors locking protocols (so it’s safe).
  • “Handles details reconfiguring emptied files”. I’m not certain what that means but I think it means that it’s safe to do things like change the store type.
  • Uses the same options as addToPersistentStore.
  • Can deadlock if you change journal modes, so be careful about those options.

The 2016 version of this session adds the detail that it’s OK to replace a persistent store that’s open. That’s nice, because I like methods that don’t make me work to keep them safe.

Hidden Messages and Buried Treasure

So much for ancient history, is anything else available? I checked NSPersistentStoreCoordinator.h. It reveals some more of the mysteries of replacePersistentStore(...).

First, although the method is designed to be safe, it’s possible to force an “unsafe” operation if you use the right options entry. This probably means you’d put it in destinationOptions but it’s not clear.

/* store option for the destroy... and replace... to indicate that the store file should be destroyed even if the operation might be unsafe (overriding locks
COREDATA_EXTERN NSString * const NSPersistentStoreForceDestroyOption API_AVAILABLE(macosx(10.8),ios(6.0));

(The lack of capitalization and the half-open parentheses are in the header file just like that).

Next, the replacePersistentStore(...) method has this short description. It’s not much but it’s more than the documentation has:

/* copy or overwrite the target persistent store in accordance with the 
store class's requirements.  It is important to pass similar options as 
addPersistentStoreWithType: ... SQLite stores will honor file locks, 
journal files, journaling modes, and other intricacies.  Other stores 
will default to using NSFileManager.

Mostly this restates details from WWDCs past, except for that last sentence. It’s interesting that it says non-SQLite stores use NSFileManager, while only mentioning SQLite details that make the replacement safe. (NS)FileManager uses APFS clones internally to copy files, this suggests SQLite doesn’t use them. The forum post says APFS cloning is used where possible, which is great, but most people use SQLite stores. Maybe there’s no efficiency benefit if you use SQLite? Who knows? (Updated 2021-03-26, thanks to Michael Tsai for the help on FileManager).

Incidentally you won’t find this if you’re using Swift and ⌘-click on the functon name. You need to find the Objective-C header. One way to do this in Xcode is to press ⌘-shift-O and start typing the class name.

Poking the Framework with a Sharp Stick

That leaves me with a fair number of questions. For example, are there any side effects I should know about? This method is touted as a modern replacement for migratePersistentStore(...). Its docs include the somewhat ominous note that

After invocation of this method, the specified store is removed from the coordinator thus store is no longer a useful reference.

I covered how to deal with that in my previous post. Is there anything like this I need to be aware of with the new method? Who knows?

So I wrote a demo app in order to try some things out, to see what I could learn by doing the digital equivalent of poking at something with a stick to see what happens. The code is a revised version of the backup/restore code I previously wrote about and which I’ll write up again soon. When in this situation I create an app project purely to test out how to use a function or class. The project isn’t a useful app but it gives me a test rig where I can try out stuff and see what happens.

Some things I learned:

  • For some operations, this method could almost be a type method instead of an instance method. In many cases it doesn’t matter if the target persistent store coordinator has loaded source or destination stores. In fact the method worked if I created one that didn’t even have a data model– I could create one with NSPersistentStoreCoordinator(). That’s not even the designated initializer, and there was a console message warning me about that, but the replacePersistentStore(...) call worked.
  • If destinationURL is a location that’s not loaded by the persistent store coordinator, there don’t appear to be any side effects to handle. The warning on migratePersistentStore(...) doesn’t apply. Not in this case anyway, however….
  • If destinationURL is a location that is loaded by the persistent store coordinator, a side-effect of the method is that the store gets unloaded and is no longer available. You can re-load it using addPersistentStore(...) after the fact to restore it.

Some things I did not learn:

  • When the method throws. Its declaration says it can throw. I tried intentionally causing some errors but it never threw. For example, what if sourceURL points to a nonexistent file? That seems like it would throw, especially since the function doesn’t return anything to indicate success or failure. It doesn’t throw, although there’s a console message reading Restore error: invalidSource("Source URL must exist").
  • When APFS cloning is possible. The forum post says this happens “where possible”, but where’s that? Changing the store type seems a likely situation. Are there others?
  • When the “force” option would be useful, and what dangers might exist around it.

This is all trial and error though, so none of it is guaranteed to be correct or to continue working this way. There are probably other important details I didn’t think of. That’s the danger of mostly undocumented API– you can try things out and learn a lot, but you’re still taking your chances that it won’t unexpectedly break.

Have Fun Storming the Castle!

With all that, what’s the verdict? Can you use this function? Should you? Well… probably. All signs point to yes. Good luck, we’re all counting on you.