Currently, our app provides a backup functionality to user, where she can perform snapshot backup of app database.
We are using migratePersistentStore
to achieve such functionality.
After running migratePersistentStore
, we will execute other I/O operations. Some of the I/O operations might fail. In such case, we need to completely delete the backup-ed database.
However, we notice it is very difficult to delete the backup-ed database!
We notice at certain point, CoreData will re-create the SQLite file, even though we have perform destroyPersistentStore
and FileManager.default.removeItem
.
Here’s our code snippet.
public static func cloneXXXDatabase(dstUrl: URL, directory: Directory, cloneTrash: Bool) -> Bool {
// Current already opened app database.
let coreDataStack = CoreDataStack.INSTANCE
guard let srcUrl = coreDataStack.persistentContainer.persistentStoreDescriptions.first?.url else { return false }
// New destination to backup current app database.
let coreDataNamedStack = CoreDataNamedStack(srcUrl)
// Have a new NSPersistentStoreCoordinator solely for migration purpose.
let psc = coreDataNamedStack.persistentContainer.persistentStoreCoordinator
// Open the SQLite. Is it fine for 2 different NSPersistentStoreCoordinator to access 1 same SQLite?
guard let srcStore = psc.persistentStore(for: srcUrl) else { return false }
do {
// Reference: https://www.avanderlee.com/swift/write-ahead-logging-wal/
// This is to ensure only 1 SQLite file is produced, without WAL & SHM.
let options = [NSSQLitePragmasOption: ["journal_mode": "DELETE"]]
try psc.migratePersistentStore(srcStore, to: dstUrl, options: options, withType: NSSQLiteStoreType)
} catch {
error_log(error)
return false
}
// ...
// ... (some other I/O operations)
// ...
var somethingWentWrong = true
if somethingWentWrong {
do {
try psc.destroyPersistentStore(at: dstUrl, ofType: NSSQLiteStoreType)
if dstUrl.exists {
try FileManager.default.removeItem(at: dstUrl)
}
print(">>>> DELETE (dstUrl)")
//
// WARNING: Such SQLite removal code is not working, and I am not sure why?!
// It seems that after returning from this function, CoreData will still re-create the backup destination
// DB, by logging
//
// CoreData: annotation: Connecting to sqlite database file at ".../xxx.sqlite".
//
} catch {
error_log(error)
}
}
return true
}
Do you have idea, how can we cleanly delete the SQLite file, which is generated via migratePersistentStore
? destroyPersistentStore
and FileManager.default.removeItem
do not seem to work.
Thank you.
Here’s the code of CoreDataStack
(Core data stack for main app), and CoreDataNamedStack
(Core data stack which points to the backup destination)
CoreDataStack (Core data stack for main app)
class CoreDataStack {
static let INSTANCE = CoreDataStack()
private init() {
}
private(set) lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "xxx", managedObjectModel: NSManagedObjectModel.wenote)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// This is a serious fatal error. We will just simply terminate the app, rather than using error_log.
fatalError("Unresolved error (error), (error.userInfo)")
}
})
// So that when backgroundContext write to persistent store, container.viewContext will retrieve update from
// persistent store.
container.viewContext.automaticallyMergesChangesFromParent = true
return container
}()
}
CoreDataNamedStack (Core data stack which points to the backup destination)
class CoreDataNamedStack: CoreDataStackable {
let url: URL
init(_ url: URL) {
self.url = url
}
private(set) lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "xxx", managedObjectModel: NSManagedObjectModel.wenote)
let storeDescription = NSPersistentStoreDescription(url: url)
container.persistentStoreDescriptions = [storeDescription]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// This is a serious fatal error. We will just simply terminate the app, rather than using error_log.
fatalError("Unresolved error (error), (error.userInfo)")
}
})
// So that when backgroundContext write to persistent store, container.viewContext will retrieve update from
// persistent store.
container.viewContext.automaticallyMergesChangesFromParent = true
return container
}()
}
Finding
Instead of executing the following deletion code immediately
var somethingWentWrong = true
if somethingWentWrong {
do {
try psc.destroyPersistentStore(at: dstUrl, ofType: NSSQLiteStoreType)
if dstUrl.exists {
try FileManager.default.removeItem(at: dstUrl)
}
print(">>>> DELETE (dstUrl)")
} catch {
error_log(error)
}
}
If we delay a few seconds before executing deletion code.
var somethingWentWrong = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
if somethingWentWrong {
do {
try psc.destroyPersistentStore(at: dstUrl, ofType: NSSQLiteStoreType)
if dstUrl.exists {
try FileManager.default.removeItem(at: dstUrl)
}
print(">>>> DELETE (dstUrl)")
} catch {
error_log(error)
}
}
}
Then such removal will success.
My guess is, CoreData has a background thread commit changes to disk. If the deletion happens before the disk changes committed, database file re-creation will happen again.
May I know what is a proper way to solve this, without having an arbitrary/ random delayed code block?
2
Answers
Observations that I have noted from your code snippets:
NSPersistentStoreCoordinator
. Apple DocumentationmigratePersistentStore()
so, you have to check if the whole migration is completed before deleting the backup-ed database.To do it you can check the status of migration and
destroyPersistentStore
ple:I am unable to test this on my end, but the
destroyPersistentStore
method or theremoveItem
method might want to be called from the main thread. That could be why it works after a delay, as theasyncAfter(deadline: .now() + 2)
method runs the code after a delay in the main thread.You could try this: