Make RecreateStoreOnModelMismatch flag work again (fixes #126)

This commit is contained in:
John Rommel Estropia
2016-11-26 16:19:12 +09:00
parent 92756fec42
commit 5d2956d674
9 changed files with 213 additions and 47 deletions

View File

@@ -29,7 +29,7 @@ import CoreStore
// MARK: - SetupTests // MARK: - SetupTests
class SetupTests: BaseTestCase { class SetupTests: BaseTestDataTestCase {
@objc @objc
dynamic func test_ThatDataStacks_ConfigureCorrectly() { 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 @objc
dynamic func test_ThatLegacySQLiteStores_SetupCorrectly() { dynamic func test_ThatLegacySQLiteStores_SetupCorrectly() {
@@ -202,7 +258,7 @@ class SetupTests: BaseTestCase {
) )
do { do {
let sqliteStore = SQLiteStore() let sqliteStore = LegacySQLiteStore()
do { do {
try stack.addStorageAndWait(sqliteStore) try stack.addStorageAndWait(sqliteStore)
@@ -217,7 +273,7 @@ class SetupTests: BaseTestCase {
} }
do { do {
let sqliteStore = SQLiteStore( let sqliteStore = LegacySQLiteStore(
fileName: "ConfigStore1.sqlite", fileName: "ConfigStore1.sqlite",
configuration: "Config1", configuration: "Config1",
localStorageOptions: .recreateStoreOnModelMismatch localStorageOptions: .recreateStoreOnModelMismatch
@@ -236,7 +292,7 @@ class SetupTests: BaseTestCase {
} }
do { do {
let sqliteStore = SQLiteStore( let sqliteStore = LegacySQLiteStore(
fileName: "ConfigStore2.sqlite", fileName: "ConfigStore2.sqlite",
configuration: "Config2", configuration: "Config2",
localStorageOptions: .recreateStoreOnModelMismatch localStorageOptions: .recreateStoreOnModelMismatch
@@ -254,4 +310,60 @@ class SetupTests: BaseTestCase {
XCTAssert(sqliteStore.matchesPersistentStore(persistentStore!)) 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()
}
}
} }

View File

@@ -312,7 +312,7 @@ public protocol LocalStorage: StorageInterface {
var mappingModelBundles: [NSBundle] { get } var mappingModelBundles: [NSBundle] { get }
var localStorageOptions: LocalStorageOptions { get } var localStorageOptions: LocalStorageOptions { get }
func dictionary(forOptions: LocalStorageOptions) -> [String: AnyObject]? 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`. If you have custom `NSIncrementalStore` or `NSAtomicStore` subclasses, you can implement this protocol and use it similarly to `SQLiteStore`.

View File

@@ -216,7 +216,10 @@ public extension DataStack {
do { do {
_ = try self.model[metadata].flatMap(storage.eraseStorageAndWait) try storage.eraseStorageAndWait(
metadata: metadata,
soureModelHint: self.model[metadata]
)
_ = try self.addStorageAndWait(storage) _ = try self.addStorageAndWait(storage)
DispatchQueue.main.async { DispatchQueue.main.async {

View File

@@ -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. 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 @objc
public func eraseStorageAndWait(soureModel: NSManagedObjectModel, error: NSErrorPointer) -> Bool { public func eraseStorageAndWait(metadata: NSDictionary, soureModelHint: NSManagedObjectModel?, error: NSErrorPointer) -> Bool {
return bridge(error) { return bridge(error) {
try self.bridgeToSwift.eraseStorageAndWait(soureModel: soureModel) try self.bridgeToSwift.eraseStorageAndWait(metadata: metadata as! [String: Any], soureModelHint: soureModelHint)
} }
} }

View File

@@ -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) 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 @objc
func eraseStorageAndWait(soureModel: NSManagedObjectModel, error: NSErrorPointer) -> Bool func eraseStorageAndWait(metadata: NSDictionary, soureModelHint: NSManagedObjectModel?, error: NSErrorPointer) -> Bool
} }

View File

@@ -261,7 +261,10 @@ public final class DataStack {
at: fileURL, at: fileURL,
options: storeOptions options: storeOptions
) )
_ = try self.model[metadata].flatMap(storage.eraseStorageAndWait) try storage.eraseStorageAndWait(
metadata: metadata,
soureModelHint: self.model[metadata]
)
_ = try self.createPersistentStoreFromStorage( _ = try self.createPersistentStoreFromStorage(
storage, storage,
finalURL: fileURL, finalURL: fileURL,

View File

@@ -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. 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 // TODO: check if attached to persistent store
let fileURL = self.fileURL func deleteFiles(storeURL: URL, extraFiles: [String] = []) throws {
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)
let fileManager = FileManager.default let fileManager = FileManager.default
let extraFiles: [String] = [
storeURL.path.appending("-wal"),
storeURL.path.appending("-shm")
]
do { 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(Bundle.main.bundleIdentifier ?? "com.CoreStore.DataStack", isDirectory: true)
.appendingPathComponent("trash", isDirectory: true) .appendingPathComponent("trash", isDirectory: true)
.appendingPathComponent(UUID().uuidString, isDirectory: false)
try fileManager.createDirectory( try fileManager.createDirectory(
at: temporaryFile.deletingLastPathComponent(), at: trashURL,
withIntermediateDirectories: true, withIntermediateDirectories: true,
attributes: nil 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 { DispatchQueue.global(qos: .background).async {
_ = try? fileManager.removeItem(at: temporaryFile) _ = try? fileManager.removeItem(at: temporaryFileURL)
extraTemporaryFiles.forEach({ _ = try? fileManager.removeItem(atPath: $0) })
} }
} }
catch { 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)
}
} }

View File

@@ -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. 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 // TODO: check if attached to persistent store
let fileURL = self.fileURL func deleteFiles(storeURL: URL, extraFiles: [String] = []) throws {
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)
let fileManager = FileManager.default let fileManager = FileManager.default
let extraFiles: [String] = [
storeURL.path.appending("-wal"),
storeURL.path.appending("-shm")
]
do { 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(Bundle.main.bundleIdentifier ?? "com.CoreStore.DataStack", isDirectory: true)
.appendingPathComponent("trash", isDirectory: true) .appendingPathComponent("trash", isDirectory: true)
.appendingPathComponent(UUID().uuidString, isDirectory: false)
try fileManager.createDirectory( try fileManager.createDirectory(
at: temporaryFile.deletingLastPathComponent(), at: trashURL,
withIntermediateDirectories: true, withIntermediateDirectories: true,
attributes: nil 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 { DispatchQueue.global(qos: .background).async {
_ = try? fileManager.removeItem(at: temporaryFile) _ = try? fileManager.removeItem(at: temporaryFileURL)
extraTemporaryFiles.forEach({ _ = try? fileManager.removeItem(atPath: $0) })
} }
} }
catch { 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)
}
} }

View File

@@ -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) 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 { internal extension LocalStorage {