Search code examples
swiftcore-datacore-data-migration

Core Data replacePersistentStore error "Failed to replace destination database"


I have a manual step by step migration in the app and it works fine besides some rare cases when replacePersistentStore(at destinationURL: URL...) fails with an error: "Error Domain=NSSQLiteErrorDomain Code=5 "(null)" UserInfo={NSFilePath=/private/var/mobile/Containers/Shared/AppGroup/430DC264-B4D6-4853-A298-E989A5AC7E5E/MyApp/MyAppStorage.sqlite, Source database Path=/private/var/mobile/Containers/Data/Application/73B79AA0-F609-4ABA-A864-D9FFBC829393/tmp/E29D6F57-3BFB-46BD-A2AD-999907876BC6/MyAppStorage.sqlite, reason=Failed to replace destination database".

I investigated this issue and tried to reproduce it but couldn't. I see these crashes on Firebase Crashlytics only. To prevent this question - I don't load store before migration completing. Maybe someone else faced the same issue or knows the reason. Here is my code:

extension NSPersistentStoreCoordinator {
static func replaceStore(
    at targetURL: URL,
    withStoreAt sourceURL: URL,
    options: [String: Any]
) {
    do {
        let persistentStoreCoordinator = NSPersistentStoreCoordinator(
            managedObjectModel: NSManagedObjectModel()
        )
        try persistentStoreCoordinator.replacePersistentStore(
            at: targetURL,
            destinationOptions: options,
            withPersistentStoreFrom: sourceURL,
            sourceOptions: options,
            ofType: NSSQLiteStoreType
        )
    } catch let error {
        fatalError("failed to replace persistent store at \(targetURL) with \(sourceURL), error: \(error)")
    }
}

...

func createTmpMigrationDirectory(url: URL) {
    do {
        try FileManager.default.createDirectory(
            at: url,
            withIntermediateDirectories: true,
            attributes: nil
        )
    } catch {
        Logger.logError("Failed to create directory at path: \(url), error: \(error)")
        fatalError("failed to create directory at path: \(url), error: \(error)")
    }
}

func removeTmpMigrationDirectory(url: URL) {
    do {
        try FileManager.default.removeItem(at: url)
    } catch {
        Logger.logError("Failed to remove item at path: \(url), error: \(error)")
    }
}

...

func migration(
    with step: CoreDataMigrationStep,
    sourceURL: URL,
    options: [String: Any]
) {
    autoreleasepool {
        let manager = NSMigrationManager(
            sourceModel: step.source,
            destinationModel: step.destination
        )
        let directoryURL = URL(
            fileURLWithPath: NSTemporaryDirectory()
        ).appendingPathComponent(UUID().uuidString)
        
        createTmpMigrationDirectory(url: directoryURL)
        
        let destinationURL = directoryURL.appendingPathComponent(
            sourceURL.lastPathComponent
        )
        
        do {
            try manager.migrateStore(
                from: sourceURL,
                sourceType: NSSQLiteStoreType,
                options: options,
                with: step.mapping,
                toDestinationURL: destinationURL,
                destinationType: NSSQLiteStoreType,
                destinationOptions: options
            )
        } catch {
            fatalError("failed attempting to migrate from \(step.source) to \(step.destination), error: \(error)")
        }
        
        NSPersistentStoreCoordinator.replaceStore(
            at: sourceURL,
            withStoreAt: destinationURL,
            options: options
        )
        
        removeTmpMigrationDirectory(url: directoryURL)
    }
}

Solution

  • There’s nothing obviously wrong with that code.

    The error message mentions SQLite error code 5, which means Core Data got an SQLITE_BUSY error. That suggests that the replace call is failing because you’re trying to replace a persistent store that’s in use somewhere. That could be in your app, or if you have app extensions it could be in one of them.

    To track this down you’ll need to find out where that’s happening. In your app, you need to be sure that you’re not using anything that could connect to the persistent store you’re replacing. If it’s an extension, you’ll need to coordinate with it somehow to make sure it doesn’t get in the way.