skip to Main Content

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


  1. Observations that I have noted from your code snippets:

    1. You used the deprecated method on NSPersistentStoreCoordinator. Apple Documentation
    2. To migrate from srcStore URL to dstStore it will take time for migration using migratePersistentStore() 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:

    let migrationResult = try psc.migratePersistentStore(srcStore, to: dstUrl, options: options, withType: NSSQLiteStoreType)
    
    if migrationResult {
        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)
        }
    }
    
    Login or Signup to reply.
  2. I am unable to test this on my end, but the destroyPersistentStore method or the removeItem method might want to be called from the main thread. That could be why it works after a delay, as the asyncAfter(deadline: .now() + 2) method runs the code after a delay in the main thread.

    You could try this:

    var somethingWentWrong = true
    
    DispatchQueue.main.async {
            
        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)
            }
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search