@@ -114,7 +114,6 @@ open class DataMigrationManager<PersistentContainerType: NSPersistentContainer>
114114 try performMigration ( )
115115 } else {
116116 migrationRunningLog. addEvent ( message: " No migration needed " )
117- cleanOldTemporaryMigrationFiles ( )
118117 }
119118 } catch {
120119 migrationRunningLog. addEvent ( message: " The migration failed: \( error. localizedDescription) . Domain: \( ( error as NSError ) . domain) " )
@@ -124,6 +123,7 @@ open class DataMigrationManager<PersistentContainerType: NSPersistentContainer>
124123 migrationRunningLog. addEvent ( message: " Creating the core data stack " )
125124
126125 self . _coreDataStack = CoreDataStack ( modelName: modelName, transactionAuthor: transactionAuthor)
126+ cleanOldTemporaryMigrationFiles ( )
127127 }
128128
129129 private var _coreDataStack : CoreDataStack < PersistentContainerType > !
@@ -162,9 +162,12 @@ open class DataMigrationManager<PersistentContainerType: NSPersistentContainer>
162162 }
163163
164164 private func getSourceStoreMetadata( storeURL: URL ) throws -> [ String : Any ] {
165- let dict = try NSPersistentStoreCoordinator . metadataForPersistentStore ( ofType: NSSQLiteStoreType,
166- at: storeURL,
167- options: nil )
165+ let dict : [ String : Any ]
166+ if #available( iOS 15 , * ) {
167+ dict = try NSPersistentStoreCoordinator . metadataForPersistentStore ( type: . sqlite, at: storeURL)
168+ } else {
169+ dict = try NSPersistentStoreCoordinator . metadataForPersistentStore ( ofType: NSSQLiteStoreType, at: storeURL)
170+ }
168171 return dict
169172 }
170173
@@ -209,17 +212,64 @@ open class DataMigrationManager<PersistentContainerType: NSPersistentContainer>
209212
210213 private func getStoreManagedObjectModel( storeURL: URL ) throws -> NSManagedObjectModel {
211214 let storeMetadata = try getSourceStoreMetadata ( storeURL: storeURL)
215+
212216 let allModels = try getAllManagedObjectModels ( )
213217 for model in allModels {
214218 if model. isConfiguration ( withName: nil , compatibleWithStoreMetadata: storeMetadata) {
215219 return model
216220 }
217221 }
222+
223+ // If we reach this point, we could not find a model compatible with the store metadata
224+ // We log a few things to debug this situation
218225 migrationRunningLog. addEvent ( message: " Could not determine the store managed object model on disk " )
226+ do {
227+ logStoreMetadataTo ( migrationRunningLog: migrationRunningLog, storeMetadata: storeMetadata)
228+ if let storeModelVersionIdentifiers = ( storeMetadata [ NSStoreModelVersionIdentifiersKey] as? NSArray ) ? . firstObject as? String {
229+ migrationRunningLog. addEvent ( message: " The store model version identifier from the store metadadata found on disk is \( storeModelVersionIdentifiers) " )
230+ if let model = allModels. first ( where: { $0. versionIdentifier == storeModelVersionIdentifiers } ) {
231+ migrationRunningLog. addEvent ( message: " We found a model having the version identifier \( storeModelVersionIdentifiers) . Logging its details now. " )
232+ logNSManagedObjectModelTo ( migrationRunningLog: migrationRunningLog, model: model)
233+ } else {
234+ migrationRunningLog. addEvent ( message: " Among all the models, we could not find a model having an identifier equal to \( storeModelVersionIdentifiers) " )
235+ }
236+ } else {
237+ migrationRunningLog. addEvent ( message: " Could not determine the store model version identifier from the store metadadata found on disk " )
238+ }
239+ }
240+
219241 throw DataMigrationManager . makeError ( message: " Could not determine the store managed object model on disk " )
220242 }
221243
222244
245+ private func logStoreMetadataTo( migrationRunningLog: RunningLogError , storeMetadata: [ String : Any ] ) {
246+ migrationRunningLog. addEvent ( message: " Content of Store Metadata on disk: " )
247+ for (key, value) in storeMetadata {
248+ if key == " NSStoreModelVersionHashes " , let modelVersionHashes = value as? [ String : Data ] {
249+ migrationRunningLog. addEvent ( message: " \( key) : " )
250+ let sortedModelVersionHashes = modelVersionHashes. sorted { $0. key < $1. key }
251+ for (key, value) in sortedModelVersionHashes {
252+ migrationRunningLog. addEvent ( message: " \( key) : \( value. hexString ( ) ) " )
253+ }
254+ } else {
255+ migrationRunningLog. addEvent ( message: " \( key) : \( String ( describing: value) ) " )
256+ }
257+ }
258+ }
259+
260+
261+ private func logNSManagedObjectModelTo( migrationRunningLog: RunningLogError , model: NSManagedObjectModel ) {
262+ let entities = model. entities. sorted { ( $0. name ?? " " ) < ( $1. name ?? " " ) }
263+ for entity in entities {
264+ if let entityName = entity. name {
265+ migrationRunningLog. addEvent ( message: " \( entityName) : \( entity. versionHash. hexString ( ) ) " )
266+ } else {
267+ migrationRunningLog. addEvent ( message: " Entity without name : \( entity. versionHash. hexString ( ) ) " )
268+ }
269+ }
270+ }
271+
272+
223273 // MARK: - Is migration needed
224274
225275 private func isMigrationNeeded( ) throws -> Bool {
@@ -235,6 +285,64 @@ open class DataMigrationManager<PersistentContainerType: NSPersistentContainer>
235285 migrationRunningLog. addEvent ( message: " Failed to get source store metadata: \( error. localizedDescription) " )
236286 os_log ( " Failed to get source store metadata: %{public}@ " , log: log, type: . fault, error. localizedDescription)
237287 logDebugInformation ( )
288+ assert ( SQLITE_CORRUPT == 11 )
289+ let nsError = error as NSError
290+ if ( nsError. domain == NSSQLiteErrorDomain && nsError. code == SQLITE_CORRUPT) ||
291+ ( nsError. domain == NSCocoaErrorDomain && nsError. code == NSPersistentStoreInvalidTypeError) {
292+ // If the database is corrupted, we know we won't be able to do anything with the file.
293+ // Before giving up, we look for another (not corrupted) .sqlite file in the same directory.
294+ // If non is found, we have no choice but to throw an error.
295+ // If one or more are found, we keep the one with the latest version, use it to replace the corrupted .sqlite file, and try again.
296+ migrationRunningLog. addEvent ( message: " [RECOVERY] Since the database is corrupted, we try to recover " )
297+ if let urlOfLatestUsableSQLiteFile = getURLOfLatestUsableSQLiteFile ( distinctFrom: storeURL) {
298+
299+ migrationRunningLog. addEvent ( message: " [RECOVERY] We found a candidate for the database replacement: \( urlOfLatestUsableSQLiteFile. lastPathComponent) " )
300+
301+ // Step 1: remove all files relating to the corrupted database (in that order shm file -> wal file -> sqlite file)
302+ let shmFile = self . storeURL. deletingPathExtension ( ) . appendingPathExtension ( " sqlite-shm " )
303+ let walFile = self . storeURL. deletingPathExtension ( ) . appendingPathExtension ( " sqlite-wal " )
304+ do {
305+ if FileManager . default. fileExists ( atPath: shmFile. path) {
306+ migrationRunningLog. addEvent ( message: " [RECOVERY] Deleting \( shmFile. lastPathComponent) " )
307+ try FileManager . default. removeItem ( at: shmFile)
308+ } else {
309+ migrationRunningLog. addEvent ( message: " [RECOVERY] No \( shmFile. lastPathComponent) to delete " )
310+ }
311+ if FileManager . default. fileExists ( atPath: walFile. path) {
312+ migrationRunningLog. addEvent ( message: " [RECOVERY] Deleting \( walFile. lastPathComponent) " )
313+ try FileManager . default. removeItem ( at: walFile)
314+ } else {
315+ migrationRunningLog. addEvent ( message: " [RECOVERY] No \( walFile. lastPathComponent) to delete " )
316+ }
317+ if FileManager . default. fileExists ( atPath: storeURL. path) {
318+ migrationRunningLog. addEvent ( message: " [RECOVERY] Deleting \( storeURL. lastPathComponent) " )
319+ try FileManager . default. removeItem ( at: storeURL)
320+ } else {
321+ migrationRunningLog. addEvent ( message: " [RECOVERY] No \( storeURL. lastPathComponent) to delete " )
322+ }
323+ }
324+
325+ // Step 2: move the latest usable SQLite file (and its associated files, in that order: sqlite file -> wal file -> shm file)
326+ let shmFileSource = urlOfLatestUsableSQLiteFile. deletingPathExtension ( ) . appendingPathExtension ( " sqlite-shm " )
327+ let walFileSource = urlOfLatestUsableSQLiteFile. deletingPathExtension ( ) . appendingPathExtension ( " sqlite-wal " )
328+ try FileManager . default. moveItem ( at: urlOfLatestUsableSQLiteFile, to: storeURL)
329+ migrationRunningLog. addEvent ( message: " [RECOVERY] Did move \( urlOfLatestUsableSQLiteFile. lastPathComponent) to \( storeURL. lastPathComponent) " )
330+ if FileManager . default. fileExists ( atPath: walFileSource. path) {
331+ try FileManager . default. moveItem ( at: walFileSource, to: walFile)
332+ migrationRunningLog. addEvent ( message: " [RECOVERY] Did move \( walFileSource. lastPathComponent) to \( walFile. lastPathComponent) " )
333+ }
334+ if FileManager . default. fileExists ( atPath: shmFileSource. path) {
335+ try FileManager . default. moveItem ( at: shmFileSource, to: shmFile)
336+ migrationRunningLog. addEvent ( message: " [RECOVERY] Did move \( shmFileSource. lastPathComponent) to \( shmFile. lastPathComponent) " )
337+ }
338+
339+ // We have replaced the corrupted database found by the best possible candidate. Now we try again.
340+
341+ migrationRunningLog. addEvent ( message: " [RECOVERY] Done with recovery operations. We test again whether a migration is needed. " )
342+ return try isMigrationNeeded ( )
343+ }
344+ migrationRunningLog. addEvent ( message: " [RECOVERY] We could not recover as no temporary SQLite file could be found " )
345+ }
238346 throw error
239347 }
240348
@@ -266,6 +374,48 @@ open class DataMigrationManager<PersistentContainerType: NSPersistentContainer>
266374 df. timeStyle = . short
267375 return df
268376 } ( )
377+
378+
379+ private func getURLOfLatestUsableSQLiteFile( distinctFrom urlToSkip: URL ) -> URL ? {
380+ migrationRunningLog. addEvent ( message: " [RECOVERY] Looking for the latest temporary SQLite file " )
381+ var urlOfLatestSQLiteFile : URL ?
382+ var modelVersionOfLatestSQLiteFile : String ?
383+ var isDirectory : ObjCBool = false
384+ if FileManager . default. fileExists ( atPath: storeURL. path, isDirectory: & isDirectory) , !isDirectory. boolValue {
385+ let storeDirectory = storeURL. deletingLastPathComponent ( )
386+ migrationRunningLog. addEvent ( message: " [RECOVERY] Looking for the latest temporary SQLite file in \( storeDirectory. path) " )
387+ if let directoryContents = try ? FileManager . default. contentsOfDirectory ( at: storeDirectory, includingPropertiesForKeys: nil ) {
388+ for file in directoryContents {
389+ guard file. pathExtension == " sqlite " && file != urlToSkip else { continue }
390+ migrationRunningLog. addEvent ( message: " [RECOVERY] Found an sqlite file: \( file. lastPathComponent) " )
391+ guard let sourceStoreMetadata = try ? getSourceStoreMetadata ( storeURL: file) else {
392+ migrationRunningLog. addEvent ( message: " [RECOVERY] Could not get metadata of file: \( file. lastPathComponent) " )
393+ continue
394+ }
395+ guard let sourceVersionIdentifier = ( sourceStoreMetadata [ NSStoreModelVersionIdentifiersKey] as? [ Any ] ) ? . first as? String else {
396+ assertionFailure ( )
397+ migrationRunningLog. addEvent ( message: " [RECOVERY] Could not get version identifier from source store metadata of file: \( file. lastPathComponent) " )
398+ continue
399+ }
400+ migrationRunningLog. addEvent ( message: " [RECOVERY] The source version identifier of file \( file. lastPathComponent) is \( sourceVersionIdentifier) " )
401+ guard ( try ? modelVersion ( sourceVersionIdentifier, isMoreRecentThan: modelVersionOfLatestSQLiteFile) ) == true else {
402+ migrationRunningLog. addEvent ( message: " [RECOVERY] The file \( file. lastPathComponent) is not the latest " )
403+ continue
404+ }
405+ // We found a new latest candidate
406+ migrationRunningLog. addEvent ( message: " [RECOVERY] We found a new candidate for the latest temporary SQLite file: \( file. lastPathComponent) " )
407+ urlOfLatestSQLiteFile = file
408+ modelVersionOfLatestSQLiteFile = sourceVersionIdentifier
409+ }
410+ }
411+ }
412+ if let urlOfLatestSQLiteFile {
413+ migrationRunningLog. addEvent ( message: " [RECOVERY] Returning \( urlOfLatestSQLiteFile. lastPathComponent) as the latest temporary sqlite file " )
414+ } else {
415+ migrationRunningLog. addEvent ( message: " [RECOVERY] We could not find any temporary sqlite file " )
416+ }
417+ return urlOfLatestSQLiteFile
418+ }
269419
270420
271421 /// As for now, this method is called when we fail to obtain (metada) information about the current version of the database and thus, fail to migrate to a new version.
@@ -633,6 +783,10 @@ open class DataMigrationManager<PersistentContainerType: NSPersistentContainer>
633783 }
634784
635785
786+ open func modelVersion( _ rawModelVersion: String , isMoreRecentThan otherRawModelVersion: String ? ) throws -> Bool {
787+ fatalError ( " Must be overwritten by subclass " )
788+ }
789+
636790}
637791
638792
0 commit comments