MacOS App Core Data app is stuck in "This NSPersistentStoreCoordinator has no persistent stores (schema mismatch or migration failure). It cannot perform a save operation."

I work on a MacOS app (which has a companion iOS app) that uses Core Data with NSPersistentCloudKitContainer.

The app also supports widgets and hence there is a need to migrate the persistent store within the core data stack using replacePersistentStore( at:....

During development I recently created a new model version and added a new entity, replaced some attributes etc...

Working on the iOS app is fine because deleting the app clears all the data allowing me to work with a clean slate.

On MacOS, I initially thought that I could simply navigate to the app group file location, delete the .sqlite file, along with the sqlite-shm and sqlite-wal. I also went and deleted the CloudKit related files.

I did all of this out of pure ignorance - my expectation was that it would give me a clean slate, but it did not.

This instead gave me some unpredictable behaviour, but the app was always in a bad state. the issues I saw were;

migration failure,

• sqlite errors highlighting no such column: t0 - where all the new entity details were missing in sqlite completely

After finding a post in the forums about how to reset macOS correctly, I did this instead -

    do {
      try container.persistentStoreCoordinator.destroyPersistentStore(at: container.persistentStoreDescriptions.first!.url!, type: .sqlite, options: nil)
      try container.persistentStoreCoordinator.destroyPersistentStore(at: storeURL, type: .sqlite, options: nil)
    } catch {
      print(String(describing: error))
    }

And now I am back to the ongoing error of This NSPersistentStoreCoordinator has no persistent stores (schema mismatch or migration failure). It cannot perform a save operation.

Another thing to note - whenever running the destroyPersistentStore( I have tried this on both the URLs of the old store location and the new one (in the app group). This still doesn't seem to help. AND I noticed that while destroyPersistentStore does get rid of the .sqlite file, it does not delete the sqlite-shm and sqlite-wal - could this be the problem? and do I need to delete these manually?

public class CoreDataManager {
  public static let shared = CoreDataManager()
  
  private enum Constants {
#if os(macOS)
    static let appGroupName = "2MM2V2959F.wabitime.group"
#elseif os(iOS)
    static let appGroupName = "group.wabitime"
#endif
    
    static let containerName = "WabiTimeDataModel"
    
    /// The name of the sql database file
    static let databaseName = "wabitime_database"
    
    /// The identifier for the container
    static let containerIdentifier = "iCloud.com.jslomowitz.WabiTime"
  }
  
  public lazy var context = persistentContainer.viewContext
  
  lazy var managedObjectModel: NSManagedObjectModel = {
    guard
      let wabiDataBundle = Bundle.module.url(
        forResource: Constants.containerName,
        withExtension: "momd"
      ),
      let managedObjectModel = NSManagedObjectModel(contentsOf: wabiDataBundle)
    else {
      assertionFailure("cannot find managedObjectModel")
      return NSManagedObjectModel()
    }
    return managedObjectModel
  }()
  
  lazy var persistentContainer: NSPersistentCloudKitContainer = {
    let container = NSPersistentCloudKitContainer(
      name: Constants.containerName,
      managedObjectModel: managedObjectModel
    )
    
    /// URL of the old sql database that has not been relocated to the app group
    var oldStoreURL: URL? {
      guard
        let storeDescription = container.persistentStoreDescriptions.first,
        let url = storeDescription.url,
        FileManager.default.fileExists(atPath: url.path)
      else {
        return nil
      }
      return url
    }
    
    /// URL of the sql database in the app group
    var storeURL: URL {
      guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupName) else {
        fatalError("Shared file container could not be created")
      }
      return fileContainer.appendingPathComponent("\(Constants.databaseName).sqlite")
    }
    
    // assign the shared container if the old store has been deleted
    if oldStoreURL == nil {
      let description = NSPersistentStoreDescription(url: storeURL)
      description.shouldInferMappingModelAutomatically = true
      description.shouldMigrateStoreAutomatically = true
      container.persistentStoreDescriptions = [description]
    }
    
    // perform store migration if necessary
    if let url = oldStoreURL, url.absoluteString != storeURL.absoluteString {
      let coordinator = container.persistentStoreCoordinator
      do {
        let storeOptions = [
          NSMigratePersistentStoresAutomaticallyOption: true,
          NSInferMappingModelAutomaticallyOption: true
        ]
        try coordinator.replacePersistentStore(
          at: url,
          withPersistentStoreFrom: storeURL,
          sourceOptions: storeOptions,
          type: .sqlite
        )
      } catch {
        print(error.localizedDescription)
      }
      self.deleteOldStore(with: url)
    }
    
    let options = NSPersistentCloudKitContainerOptions(containerIdentifier: Constants.containerIdentifier)
    guard let description = container.persistentStoreDescriptions.first else {
      fatalError("Could not retrieve a persistent store description.")
    }
    description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
    description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
    description.cloudKitContainerOptions = options
    
    container.loadPersistentStores(completionHandler: { [weak self] (_, error) in
      guard let self, error == nil else {
        assertionFailure("Unresolved error: \(String(describing: error))")
        return
      }
    })
    
    container.viewContext.automaticallyMergesChangesFromParent = true
    container.viewContext.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
    
    return container
  }()
  
  private func deleteOldStore(with url: URL) {
    let fileCoordinator = NSFileCoordinator()
    fileCoordinator.coordinate(writingItemAt: url, options: .forDeleting, error: nil) { url in
      do {
        try FileManager.default.removeItem(at: url)
      } catch {
        print(error.localizedDescription)
      }
    }
  }
  
  // MARK: - Core Data Saving and Undo support
  func saveContext(completion: (() -> Void)? = nil) {
    #if os(macOS)
    if !context.commitEditing() {
      NSLog("AppDelegate unable to commit editing before saving")
    }
    #endif
    if context.hasChanges {
      do {
        try context.save()
        print("SAVED")
        completion?()
      } catch {
        let nserror = error as NSError
        #if os(macOS)
        NSApplication.shared.presentError(nserror)
        #endif
      }
    }
  }
}
  • Another question that comes up is that over the course of 4 years we have on occasion had MacOS users that complain that their app keeps crashing and they can't use it anymore, even if they delete the app entirely from their system. We have tried offering the advice to delete residual app files (sqlite and the other CloudKit ones which seem to stick around)

    For some users, this seems to work and others it doesn't work at all. What do we do about this?

  • And in the case of complete migration failure, is there a way to rebuild the database, and how would we go about doing that?

Add a Comment