From 5d2956d67476d2906beb33b505dc95cba0e9dc29 Mon Sep 17 00:00:00 2001 From: John Rommel Estropia Date: Sat, 26 Nov 2016 16:19:12 +0900 Subject: [PATCH] Make RecreateStoreOnModelMismatch flag work again (fixes #126) --- CoreStoreTests/SetupTests.swift | 120 +++++++++++++++++- README.md | 2 +- Sources/Migrating/DataStack+Migration.swift | 5 +- Sources/ObjectiveC/CSSQliteStore.swift | 4 +- Sources/ObjectiveC/CSStorageInterface.swift | 2 +- Sources/Setup/DataStack.swift | 5 +- .../StorageInterfaces/LegacySQLiteStore.swift | 60 ++++++--- .../Setup/StorageInterfaces/SQLiteStore.swift | 60 ++++++--- .../StorageInterfaces/StorageInterface.swift | 2 +- 9 files changed, 213 insertions(+), 47 deletions(-) diff --git a/CoreStoreTests/SetupTests.swift b/CoreStoreTests/SetupTests.swift index f774046..4be0452 100644 --- a/CoreStoreTests/SetupTests.swift +++ b/CoreStoreTests/SetupTests.swift @@ -29,7 +29,7 @@ import CoreStore // MARK: - SetupTests -class SetupTests: BaseTestCase { +class SetupTests: BaseTestDataTestCase { @objc dynamic func test_ThatDataStacks_ConfigureCorrectly() { @@ -193,6 +193,62 @@ class SetupTests: BaseTestCase { } } + @objc + dynamic func test_ThatSQLiteStores_DeleteFilesCorrectly() { + + let fileManager = FileManager.default + let sqliteStore = SQLiteStore() + func createStore() throws -> [String: Any] { + + do { + + let stack = DataStack( + modelName: "Model", + bundle: Bundle(for: type(of: self)) + ) + try! stack.addStorageAndWait(sqliteStore) + self.prepareTestDataForStack(stack) + } + XCTAssertTrue(fileManager.fileExists(atPath: sqliteStore.fileURL.path)) + XCTAssertTrue(fileManager.fileExists(atPath: sqliteStore.fileURL.path.appending("-wal"))) + XCTAssertTrue(fileManager.fileExists(atPath: sqliteStore.fileURL.path.appending("-shm"))) + + return try NSPersistentStoreCoordinator.metadataForPersistentStore( + ofType: type(of: sqliteStore).storeType, + at: sqliteStore.fileURL, + options: sqliteStore.storeOptions + ) + } + do { + + let metadata = try createStore() + let stack = DataStack( + modelName: "Model", + bundle: Bundle(for: type(of: self)) + ) + try sqliteStore.eraseStorageAndWait(metadata: metadata, soureModelHint: stack.model[metadata]) + XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path)) + XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path.appending("-wal"))) + XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path.appending("-shm"))) + } + catch { + + XCTFail() + } + do { + + let metadata = try createStore() + try sqliteStore.eraseStorageAndWait(metadata: metadata, soureModelHint: nil) + XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path)) + XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path.appending("-wal"))) + XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path.appending("-shm"))) + } + catch { + + XCTFail() + } + } + @objc dynamic func test_ThatLegacySQLiteStores_SetupCorrectly() { @@ -202,7 +258,7 @@ class SetupTests: BaseTestCase { ) do { - let sqliteStore = SQLiteStore() + let sqliteStore = LegacySQLiteStore() do { try stack.addStorageAndWait(sqliteStore) @@ -217,7 +273,7 @@ class SetupTests: BaseTestCase { } do { - let sqliteStore = SQLiteStore( + let sqliteStore = LegacySQLiteStore( fileName: "ConfigStore1.sqlite", configuration: "Config1", localStorageOptions: .recreateStoreOnModelMismatch @@ -236,7 +292,7 @@ class SetupTests: BaseTestCase { } do { - let sqliteStore = SQLiteStore( + let sqliteStore = LegacySQLiteStore( fileName: "ConfigStore2.sqlite", configuration: "Config2", localStorageOptions: .recreateStoreOnModelMismatch @@ -254,4 +310,60 @@ class SetupTests: BaseTestCase { XCTAssert(sqliteStore.matchesPersistentStore(persistentStore!)) } } + + @objc + dynamic func test_ThatLegacySQLiteStores_DeleteFilesCorrectly() { + + let fileManager = FileManager.default + let sqliteStore = LegacySQLiteStore() + func createStore() throws -> [String: Any] { + + do { + + let stack = DataStack( + modelName: "Model", + bundle: Bundle(for: type(of: self)) + ) + try! stack.addStorageAndWait(sqliteStore) + self.prepareTestDataForStack(stack) + } + XCTAssertTrue(fileManager.fileExists(atPath: sqliteStore.fileURL.path)) + XCTAssertTrue(fileManager.fileExists(atPath: sqliteStore.fileURL.path.appending("-wal"))) + XCTAssertTrue(fileManager.fileExists(atPath: sqliteStore.fileURL.path.appending("-shm"))) + + return try NSPersistentStoreCoordinator.metadataForPersistentStore( + ofType: type(of: sqliteStore).storeType, + at: sqliteStore.fileURL, + options: sqliteStore.storeOptions + ) + } + do { + + let metadata = try createStore() + let stack = DataStack( + modelName: "Model", + bundle: Bundle(for: type(of: self)) + ) + try sqliteStore.eraseStorageAndWait(metadata: metadata, soureModelHint: stack.model[metadata]) + XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path)) + XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path.appending("-wal"))) + XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path.appending("-shm"))) + } + catch { + + XCTFail() + } + do { + + let metadata = try createStore() + try sqliteStore.eraseStorageAndWait(metadata: metadata, soureModelHint: nil) + XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path)) + XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path.appending("-wal"))) + XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path.appending("-shm"))) + } + catch { + + XCTFail() + } + } } diff --git a/README.md b/README.md index c848e60..968fafd 100644 --- a/README.md +++ b/README.md @@ -312,7 +312,7 @@ public protocol LocalStorage: StorageInterface { var mappingModelBundles: [NSBundle] { get } var localStorageOptions: LocalStorageOptions { get } func dictionary(forOptions: LocalStorageOptions) -> [String: AnyObject]? - func eraseStorageAndWait(soureModel: NSManagedObjectModel) throws + func eraseStorageAndWait(metadata: [String: Any], soureModelHint: NSManagedObjectModel?) throws } ``` If you have custom `NSIncrementalStore` or `NSAtomicStore` subclasses, you can implement this protocol and use it similarly to `SQLiteStore`. diff --git a/Sources/Migrating/DataStack+Migration.swift b/Sources/Migrating/DataStack+Migration.swift index 8bc6728..6b51bd0 100644 --- a/Sources/Migrating/DataStack+Migration.swift +++ b/Sources/Migrating/DataStack+Migration.swift @@ -216,7 +216,10 @@ public extension DataStack { do { - _ = try self.model[metadata].flatMap(storage.eraseStorageAndWait) + try storage.eraseStorageAndWait( + metadata: metadata, + soureModelHint: self.model[metadata] + ) _ = try self.addStorageAndWait(storage) DispatchQueue.main.async { diff --git a/Sources/ObjectiveC/CSSQliteStore.swift b/Sources/ObjectiveC/CSSQliteStore.swift index 0dca8c3..f449c76 100644 --- a/Sources/ObjectiveC/CSSQliteStore.swift +++ b/Sources/ObjectiveC/CSSQliteStore.swift @@ -154,11 +154,11 @@ public final class CSSQLiteStore: NSObject, CSLocalStorage, CoreStoreObjectiveCT Called by the `CSDataStack` to perform actual deletion of the store file from disk. Do not call directly! The `sourceModel` argument is a hint for the existing store's model version. For `CSSQLiteStore`, this converts the database's WAL journaling mode to DELETE before deleting the file. */ @objc - public func eraseStorageAndWait(soureModel: NSManagedObjectModel, error: NSErrorPointer) -> Bool { + public func eraseStorageAndWait(metadata: NSDictionary, soureModelHint: NSManagedObjectModel?, error: NSErrorPointer) -> Bool { return bridge(error) { - try self.bridgeToSwift.eraseStorageAndWait(soureModel: soureModel) + try self.bridgeToSwift.eraseStorageAndWait(metadata: metadata as! [String: Any], soureModelHint: soureModelHint) } } diff --git a/Sources/ObjectiveC/CSStorageInterface.swift b/Sources/ObjectiveC/CSStorageInterface.swift index 0935565..44681f3 100644 --- a/Sources/ObjectiveC/CSStorageInterface.swift +++ b/Sources/ObjectiveC/CSStorageInterface.swift @@ -121,5 +121,5 @@ public protocol CSLocalStorage: CSStorageInterface { Called by the `CSDataStack` to perform actual deletion of the store file from disk. Do not call directly! The `sourceModel` argument is a hint for the existing store's model version. Implementers can use the `sourceModel` to perform necessary store operations. (SQLite stores for example, can convert WAL journaling mode to DELETE before deleting) */ @objc - func eraseStorageAndWait(soureModel: NSManagedObjectModel, error: NSErrorPointer) -> Bool + func eraseStorageAndWait(metadata: NSDictionary, soureModelHint: NSManagedObjectModel?, error: NSErrorPointer) -> Bool } diff --git a/Sources/Setup/DataStack.swift b/Sources/Setup/DataStack.swift index 9dfea89..abdcd6a 100644 --- a/Sources/Setup/DataStack.swift +++ b/Sources/Setup/DataStack.swift @@ -261,7 +261,10 @@ public final class DataStack { at: fileURL, options: storeOptions ) - _ = try self.model[metadata].flatMap(storage.eraseStorageAndWait) + try storage.eraseStorageAndWait( + metadata: metadata, + soureModelHint: self.model[metadata] + ) _ = try self.createPersistentStoreFromStorage( storage, finalURL: fileURL, diff --git a/Sources/Setup/StorageInterfaces/LegacySQLiteStore.swift b/Sources/Setup/StorageInterfaces/LegacySQLiteStore.swift index 85ca37c..e0e1c0a 100644 --- a/Sources/Setup/StorageInterfaces/LegacySQLiteStore.swift +++ b/Sources/Setup/StorageInterfaces/LegacySQLiteStore.swift @@ -165,45 +165,69 @@ public final class LegacySQLiteStore: LocalStorage, DefaultInitializableStore { /** Called by the `DataStack` to perform actual deletion of the store file from disk. Do not call directly! The `sourceModel` argument is a hint for the existing store's model version. For `SQLiteStore`, this converts the database's WAL journaling mode to DELETE before deleting the file. */ - public func eraseStorageAndWait(soureModel: NSManagedObjectModel) throws { + public func eraseStorageAndWait(metadata: [String: Any], soureModelHint: NSManagedObjectModel?) throws { // TODO: check if attached to persistent store - let fileURL = self.fileURL - try autoreleasepool { - - let journalUpdatingCoordinator = NSPersistentStoreCoordinator(managedObjectModel: soureModel) - let store = try journalUpdatingCoordinator.addPersistentStore( - ofType: type(of: self).storeType, - configurationName: self.configuration, - at: fileURL, - options: [NSSQLitePragmasOption: ["journal_mode": "DELETE"]] - ) - try journalUpdatingCoordinator.remove(store) + func deleteFiles(storeURL: URL, extraFiles: [String] = []) throws { let fileManager = FileManager.default + let extraFiles: [String] = [ + storeURL.path.appending("-wal"), + storeURL.path.appending("-shm") + ] do { - let temporaryFile = URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first!) + let trashURL = URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first!) .appendingPathComponent(Bundle.main.bundleIdentifier ?? "com.CoreStore.DataStack", isDirectory: true) .appendingPathComponent("trash", isDirectory: true) - .appendingPathComponent(UUID().uuidString, isDirectory: false) try fileManager.createDirectory( - at: temporaryFile.deletingLastPathComponent(), + at: trashURL, withIntermediateDirectories: true, attributes: nil ) - try fileManager.moveItem(at: fileURL, to: temporaryFile) + + let temporaryFileURL = trashURL.appendingPathComponent(UUID().uuidString, isDirectory: false) + try fileManager.moveItem(at: storeURL, to: temporaryFileURL) + + let extraTemporaryFiles = extraFiles.map { (extraFile) -> String in + + let temporaryFile = trashURL.appendingPathComponent(UUID().uuidString, isDirectory: false).path + if let _ = try? fileManager.moveItem(atPath: extraFile, toPath: temporaryFile) { + + return temporaryFile + } + return extraFile + } DispatchQueue.global(qos: .background).async { - _ = try? fileManager.removeItem(at: temporaryFile) + _ = try? fileManager.removeItem(at: temporaryFileURL) + extraTemporaryFiles.forEach({ _ = try? fileManager.removeItem(atPath: $0) }) } } catch { - try fileManager.removeItem(at: fileURL) + try fileManager.removeItem(at: storeURL) + extraFiles.forEach({ _ = try? fileManager.removeItem(atPath: $0) }) } } + + let fileURL = self.fileURL + try autoreleasepool { + + if let soureModel = soureModelHint ?? NSManagedObjectModel.mergedModel(from: nil, forStoreMetadata: metadata) { + + let journalUpdatingCoordinator = NSPersistentStoreCoordinator(managedObjectModel: soureModel) + let store = try journalUpdatingCoordinator.addPersistentStore( + ofType: type(of: self).storeType, + configurationName: self.configuration, + at: fileURL, + options: [NSSQLitePragmasOption: ["journal_mode": "DELETE"]] + ) + try journalUpdatingCoordinator.remove(store) + } + try deleteFiles(storeURL: fileURL) + } } diff --git a/Sources/Setup/StorageInterfaces/SQLiteStore.swift b/Sources/Setup/StorageInterfaces/SQLiteStore.swift index 37858d6..815b9d7 100644 --- a/Sources/Setup/StorageInterfaces/SQLiteStore.swift +++ b/Sources/Setup/StorageInterfaces/SQLiteStore.swift @@ -162,45 +162,69 @@ public final class SQLiteStore: LocalStorage, DefaultInitializableStore { /** Called by the `DataStack` to perform actual deletion of the store file from disk. Do not call directly! The `sourceModel` argument is a hint for the existing store's model version. For `SQLiteStore`, this converts the database's WAL journaling mode to DELETE before deleting the file. */ - public func eraseStorageAndWait(soureModel: NSManagedObjectModel) throws { + public func eraseStorageAndWait(metadata: [String: Any], soureModelHint: NSManagedObjectModel?) throws { // TODO: check if attached to persistent store - let fileURL = self.fileURL - try autoreleasepool { - - let journalUpdatingCoordinator = NSPersistentStoreCoordinator(managedObjectModel: soureModel) - let store = try journalUpdatingCoordinator.addPersistentStore( - ofType: type(of: self).storeType, - configurationName: self.configuration, - at: fileURL, - options: [NSSQLitePragmasOption: ["journal_mode": "DELETE"]] - ) - try journalUpdatingCoordinator.remove(store) + func deleteFiles(storeURL: URL, extraFiles: [String] = []) throws { let fileManager = FileManager.default + let extraFiles: [String] = [ + storeURL.path.appending("-wal"), + storeURL.path.appending("-shm") + ] do { - let temporaryFile = URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first!) + let trashURL = URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first!) .appendingPathComponent(Bundle.main.bundleIdentifier ?? "com.CoreStore.DataStack", isDirectory: true) .appendingPathComponent("trash", isDirectory: true) - .appendingPathComponent(UUID().uuidString, isDirectory: false) try fileManager.createDirectory( - at: temporaryFile.deletingLastPathComponent(), + at: trashURL, withIntermediateDirectories: true, attributes: nil ) - try fileManager.moveItem(at: fileURL, to: temporaryFile) + + let temporaryFileURL = trashURL.appendingPathComponent(UUID().uuidString, isDirectory: false) + try fileManager.moveItem(at: storeURL, to: temporaryFileURL) + + let extraTemporaryFiles = extraFiles.map { (extraFile) -> String in + + let temporaryFile = trashURL.appendingPathComponent(UUID().uuidString, isDirectory: false).path + if let _ = try? fileManager.moveItem(atPath: extraFile, toPath: temporaryFile) { + + return temporaryFile + } + return extraFile + } DispatchQueue.global(qos: .background).async { - _ = try? fileManager.removeItem(at: temporaryFile) + _ = try? fileManager.removeItem(at: temporaryFileURL) + extraTemporaryFiles.forEach({ _ = try? fileManager.removeItem(atPath: $0) }) } } catch { - try fileManager.removeItem(at: fileURL) + try fileManager.removeItem(at: storeURL) + extraFiles.forEach({ _ = try? fileManager.removeItem(atPath: $0) }) } } + + let fileURL = self.fileURL + try autoreleasepool { + + if let soureModel = soureModelHint ?? NSManagedObjectModel.mergedModel(from: nil, forStoreMetadata: metadata) { + + let journalUpdatingCoordinator = NSPersistentStoreCoordinator(managedObjectModel: soureModel) + let store = try journalUpdatingCoordinator.addPersistentStore( + ofType: type(of: self).storeType, + configurationName: self.configuration, + at: fileURL, + options: [NSSQLitePragmasOption: ["journal_mode": "DELETE"]] + ) + try journalUpdatingCoordinator.remove(store) + } + try deleteFiles(storeURL: fileURL) + } } diff --git a/Sources/Setup/StorageInterfaces/StorageInterface.swift b/Sources/Setup/StorageInterfaces/StorageInterface.swift index 84a459b..28444bb 100644 --- a/Sources/Setup/StorageInterfaces/StorageInterface.swift +++ b/Sources/Setup/StorageInterfaces/StorageInterface.swift @@ -158,7 +158,7 @@ public protocol LocalStorage: StorageInterface { /** Called by the `DataStack` to perform actual deletion of the store file from disk. **Do not call directly!** The `sourceModel` argument is a hint for the existing store's model version. Implementers can use the `sourceModel` to perform necessary store operations. (SQLite stores for example, can convert WAL journaling mode to DELETE before deleting) */ - func eraseStorageAndWait(soureModel: NSManagedObjectModel) throws + func eraseStorageAndWait(metadata: [String: Any], soureModelHint: NSManagedObjectModel?) throws } internal extension LocalStorage {