WIP: allow migrations for CoreStoreObjects

This commit is contained in:
John Estropia
2017-04-11 19:13:35 +09:00
parent e5ef4992d3
commit 9f3db61ff7
27 changed files with 1146 additions and 691 deletions

View File

@@ -222,7 +222,7 @@ public enum CoreStoreError: Error, CustomNSError, Hashable {
internal init(_ error: Error?) {
self = error.flatMap { $0.bridgeToSwift } ?? .unknown
self = error.flatMap({ $0.bridgeToSwift }) ?? .unknown
}
}

View File

@@ -26,12 +26,12 @@
import Foundation
// MARK: - XcdatamodelFilename
// MARK: - XcodeDataModelFileName
/**
A `String` that pertains to the name of an *.xcdatamodeld file (without the file extension).
*/
public typealias XcdatamodelFilename = String
public typealias XcodeDataModelFileName = String
// MARK: - ModelConfiguration

View File

@@ -580,11 +580,11 @@ extension UUID: ImportableAttributeType {
enum Static {
static let empty: UUID = {
static let empty: UUID = cs_lazy {
var zero = Array<UInt8>(repeating: 0, count: 16)
return NSUUID(uuidBytes: &zero) as UUID
}()
}
}
return Static.empty
}

View File

@@ -0,0 +1,106 @@
//
// EntityIdentifier.swift
// CoreStore
//
// Copyright © 2017 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import CoreData
import Foundation
// MARK: - EntityIdentifier
internal struct EntityIdentifier: Hashable {
// MARK: - Category
internal enum Category: Int {
case coreData
case coreStore
}
// MARK: -
internal let category: Category
internal let interfacedClassName: String
internal init(_ type: NSManagedObject.Type) {
self.category = .coreData
self.interfacedClassName = String(reflecting: type)
}
internal init(_ type: CoreStoreObject.Type) {
self.category = .coreStore
self.interfacedClassName = String(reflecting: type)
}
internal init(_ type: DynamicObject.Type) {
switch type {
case let type as NSManagedObject.Type:
self.init(type)
case let type as CoreStoreObject.Type:
self.init(type)
default:
CoreStore.abort("\(cs_typeName(DynamicObject.self)) is not meant to be implemented by external types.")
}
}
internal init(_ entityDescription: NSEntityDescription) {
if let anyEntity = entityDescription.anyEntity {
self.category = .coreStore
self.interfacedClassName = NSStringFromClass(anyEntity.type)
}
else {
self.category = .coreData
self.interfacedClassName = entityDescription.managedObjectClassName!
}
}
// MARK: Equatable
static func == (lhs: EntityIdentifier, rhs: EntityIdentifier) -> Bool {
return lhs.category == rhs.category
&& lhs.interfacedClassName == rhs.interfacedClassName
}
// MARK: Hashable
var hashValue: Int {
return self.category.hashValue
^ self.interfacedClassName.hashValue
}
}

View File

@@ -100,3 +100,12 @@ internal func cs_typeName(_ name: String?) -> String {
return "<\(name ?? "unknown")>"
}
// MARK: Functional
@inline(__always)
internal func cs_lazy<T>(_ closure: () -> T) -> T {
return closure()
}

View File

@@ -0,0 +1,76 @@
//
// NSEntityDescription+DynamicModel.swift
// CoreStore
//
// Copyright © 2017 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import CoreData
import Foundation
// MARK: - NSEntityDescription
internal extension NSEntityDescription {
@nonobjc
internal var anyEntity: CoreStoreSchema.AnyEntity? {
get {
guard let userInfo = self.userInfo,
let typeName = userInfo[UserInfoKey.CoreStoreManagedObjectTypeName] as! String?,
let entityName = userInfo[UserInfoKey.CoreStoreManagedObjectEntityName] as! String? else {
return nil
}
return CoreStoreSchema.AnyEntity(
type: NSClassFromString(typeName) as! CoreStoreObject.Type,
entityName: entityName
)
}
set {
if let newValue = newValue {
self.userInfo = [
UserInfoKey.CoreStoreManagedObjectTypeName: NSStringFromClass(newValue.type),
UserInfoKey.CoreStoreManagedObjectEntityName: newValue.entityName
]
}
else {
self.userInfo = [:]
}
}
}
// MARK: Private
// MARK: - UserInfoKey
fileprivate enum UserInfoKey {
fileprivate static let CoreStoreManagedObjectTypeName = "CoreStoreManagedObjectTypeName"
fileprivate static let CoreStoreManagedObjectEntityName = "CoreStoreManagedObjectEntityName"
}
}

View File

@@ -1,274 +0,0 @@
//
// NSManagedObjectModel+Setup.swift
// CoreStore
//
// Copyright © 2015 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import Foundation
import CoreData
// MARK: - NSManagedObjectModel
internal extension NSManagedObjectModel {
// MARK: Internal
@nonobjc
internal static func fromBundle(_ bundle: Bundle, modelName: String, modelVersionHints: Set<String> = []) -> NSManagedObjectModel {
guard let modelFilePath = bundle.path(forResource: modelName, ofType: "momd") else {
// For users migrating from very old Xcode versions: Old xcdatamodel files are not contained inside xcdatamodeld (with a "d"), and will thus fail this check. If that was the case, create a new xcdatamodeld file and copy all contents into the new model.
let foundModels = bundle
.paths(forResourcesOfType: "momd", inDirectory: nil)
.map({ ($0 as NSString).lastPathComponent })
CoreStore.abort("Could not find \"\(modelName).momd\" from the bundle \"\(bundle.bundleIdentifier ?? "<nil>")\". Other model files in bundle: \(foundModels.coreStoreDumpString)")
}
let modelFileURL = URL(fileURLWithPath: modelFilePath)
let versionInfoPlistURL = modelFileURL.appendingPathComponent("VersionInfo.plist", isDirectory: false)
guard let versionInfo = NSDictionary(contentsOf: versionInfoPlistURL),
let versionHashes = versionInfo["NSManagedObjectModel_VersionHashes"] as? [String: AnyObject] else {
CoreStore.abort("Could not load \(cs_typeName(NSManagedObjectModel.self)) metadata from path \"\(versionInfoPlistURL)\".")
}
let modelVersions = Set(versionHashes.keys)
let currentModelVersion: String
if let plistModelVersion = versionInfo["NSManagedObjectModel_CurrentVersionName"] as? String,
modelVersionHints.isEmpty || modelVersionHints.contains(plistModelVersion) {
currentModelVersion = plistModelVersion
}
else if let resolvedVersion = modelVersions.intersection(modelVersionHints).first {
CoreStore.log(
.warning,
message: "The MigrationChain leaf versions do not include the model file's current version. Resolving to version \"\(resolvedVersion)\"."
)
currentModelVersion = resolvedVersion
}
else if let resolvedVersion = modelVersions.first ?? modelVersionHints.first {
if !modelVersionHints.isEmpty {
CoreStore.log(
.warning,
message: "The MigrationChain leaf versions do not include any of the model file's embedded versions. Resolving to version \"\(resolvedVersion)\"."
)
}
currentModelVersion = resolvedVersion
}
else {
CoreStore.abort("No model files were found in URL \"\(modelFileURL)\".")
}
var modelVersionFileURL: URL?
for modelVersion in modelVersions {
let fileURL = modelFileURL.appendingPathComponent("\(modelVersion).mom", isDirectory: false)
if modelVersion == currentModelVersion {
modelVersionFileURL = fileURL
continue
}
precondition(
NSManagedObjectModel(contentsOf: fileURL) != nil,
"Could not find the \"\(modelVersion).mom\" version file for the model at URL \"\(modelFileURL)\"."
)
}
if let modelVersionFileURL = modelVersionFileURL,
let rootModel = NSManagedObjectModel(contentsOf: modelVersionFileURL) {
// TODO: apply to DynamicModel as well
rootModel.modelVersionFileURL = modelVersionFileURL
rootModel.modelVersions = modelVersions
rootModel.currentModelVersion = currentModelVersion
return rootModel
}
CoreStore.abort("Could not create an \(cs_typeName(NSManagedObjectModel.self)) from the model at URL \"\(modelFileURL)\".")
}
@nonobjc
internal private(set) var currentModelVersion: String? {
get {
let value: NSString? = cs_getAssociatedObjectForKey(
&PropertyKeys.currentModelVersion,
inObject: self
)
return value as String?
}
set {
cs_setAssociatedCopiedObject(
newValue == nil ? nil : (newValue! as NSString),
forKey: &PropertyKeys.currentModelVersion,
inObject: self
)
}
}
@nonobjc
internal private(set) var modelVersions: Set<String>? {
get {
let value: NSSet? = cs_getAssociatedObjectForKey(
&PropertyKeys.modelVersions,
inObject: self
)
return value as? Set<String>
}
set {
cs_setAssociatedCopiedObject(
newValue == nil ? nil : (newValue! as NSSet),
forKey: &PropertyKeys.modelVersions,
inObject: self
)
}
}
@nonobjc
internal var entityDescriptionsByEntityIdentifier: [EntityIdentifier: NSEntityDescription] {
if let mapping: NSDictionary = cs_getAssociatedObjectForKey(&PropertyKeys.objectClassNamesByEntityName, inObject: self) {
return mapping as! [EntityIdentifier: NSEntityDescription]
}
var mapping: [EntityIdentifier: NSEntityDescription] = [:]
self.entities.forEach { (entityDescription) in
let entityIdentifier = EntityIdentifier(entityDescription)
mapping[entityIdentifier] = entityDescription
}
cs_setAssociatedCopiedObject(
mapping as NSDictionary,
forKey: &PropertyKeys.objectClassNamesByEntityName,
inObject: self
)
return mapping
}
@nonobjc
internal func mergedModels() -> [NSManagedObjectModel] {
return self.modelVersions?.map { self[$0] }.flatMap { $0 == nil ? [] : [$0!] } ?? [self]
}
@nonobjc
internal subscript(modelVersion: String) -> NSManagedObjectModel? {
if modelVersion == self.currentModelVersion {
return self
}
guard let modelFileURL = self.modelFileURL,
let modelVersions = self.modelVersions,
modelVersions.contains(modelVersion) else {
return nil
}
// TODO: apply to DynamicModel as well
let versionModelFileURL = modelFileURL.appendingPathComponent("\(modelVersion).mom", isDirectory: false)
guard let model = NSManagedObjectModel(contentsOf: versionModelFileURL) else {
return nil
}
model.currentModelVersion = modelVersion
model.modelVersionFileURL = versionModelFileURL
model.modelVersions = modelVersions
return model
}
@nonobjc
internal subscript(metadata: [String: Any]) -> NSManagedObjectModel? {
guard let modelHashes = metadata[NSStoreModelVersionHashesKey] as? [String : Data] else {
return nil
}
for modelVersion in self.modelVersions ?? [] {
if let versionModel = self[modelVersion], modelHashes == versionModel.entityVersionHashesByName {
return versionModel
}
}
return nil
}
// MARK: Private
@nonobjc
private var modelFileURL: URL? {
get {
return self.modelVersionFileURL?.deletingLastPathComponent()
}
}
@nonobjc
private var modelVersionFileURL: URL? {
get {
let value: NSURL? = cs_getAssociatedObjectForKey(
&PropertyKeys.modelVersionFileURL,
inObject: self
)
return value as URL?
}
set {
cs_setAssociatedCopiedObject(
newValue as NSURL?,
forKey: &PropertyKeys.modelVersionFileURL,
inObject: self
)
}
}
private struct PropertyKeys {
static var objectClassNamesByEntityName: Void?
static var modelVersionFileURL: Void?
static var modelVersions: Void?
static var currentModelVersion: Void?
}
}

View File

@@ -158,6 +158,32 @@ extension CoreStoreError: CustomDebugStringConvertible, CoreStoreDebugStringConv
}
// MARK: - CoreStoreSchema
extension CoreStoreSchema: CustomDebugStringConvertible, CoreStoreDebugStringConvertible {
// MARK: CustomDebugStringConvertible
public var debugDescription: String {
return formattedDebugDescription(self)
}
// MARK: CoreStoreDebugStringConvertible
public var coreStoreDumpString: String {
return createFormattedString(
"(", ")",
("modelVersion", self.modelVersion),
("entitiesByConfiguration", self.entitiesByConfiguration),
("rawModel", self.rawModel())
)
}
}
// MARK: - DataStack
extension DataStack: CustomDebugStringConvertible, CoreStoreDebugStringConvertible {
@@ -179,14 +205,38 @@ extension DataStack: CustomDebugStringConvertible, CoreStoreDebugStringConvertib
("coordinator", self.coordinator),
("rootSavingContext", self.rootSavingContext),
("mainContext", self.mainContext),
("model", self.model),
("migrationChain", self.migrationChain),
("schemaHistory", self.schemaHistory),
("coordinator.persistentStores", self.coordinator.persistentStores)
)
}
}
// MARK: - Entity
extension Entity: CustomDebugStringConvertible, CoreStoreDebugStringConvertible {
// MARK: CustomDebugStringConvertible
public var debugDescription: String {
return formattedDebugDescription(self)
}
// MARK: CoreStoreDebugStringConvertible
public var coreStoreDumpString: String {
return createFormattedString(
"(", ")",
("type", self.type),
("entityName", self.entityName)
)
}
}
// MARK: - From
extension From: CustomDebugStringConvertible, CoreStoreDebugStringConvertible {
@@ -350,6 +400,31 @@ extension LegacySQLiteStore: CustomDebugStringConvertible, CoreStoreDebugStringC
}
// MARK: - LegacyXcodeDataModel
extension LegacyXcodeDataModel: CustomDebugStringConvertible, CoreStoreDebugStringConvertible {
// MARK: CustomDebugStringConvertible
public var debugDescription: String {
return formattedDebugDescription(self)
}
// MARK: CoreStoreDebugStringConvertible
public var coreStoreDumpString: String {
return createFormattedString(
"(", ")",
("modelVersion", self.modelVersion),
("rawModel", self.rawModel())
)
}
}
// MARK: - ListMonitor
@available(OSX 10.12, *)
@@ -456,7 +531,7 @@ extension MigrationChain: CustomDebugStringConvertible, CoreStoreDebugStringConv
public var coreStoreDumpString: String {
guard self.valid else {
guard self.isValid else {
return "<invalid migration chain>"
}
@@ -669,6 +744,32 @@ extension SectionBy: CustomDebugStringConvertible, CoreStoreDebugStringConvertib
}
// MARK: - SchemaHistory
extension SchemaHistory: CustomDebugStringConvertible, CoreStoreDebugStringConvertible {
// MARK: CustomDebugStringConvertible
public var debugDescription: String {
return formattedDebugDescription(self)
}
// MARK: CoreStoreDebugStringConvertible
public var coreStoreDumpString: String {
return createFormattedString(
"(", ")",
("currentModelVersion", self.currentModelVersion),
("migrationChain", self.migrationChain),
("schemaByVersion", self.schemaByVersion)
)
}
}
// MARK: - Select
extension Select: CustomDebugStringConvertible, CoreStoreDebugStringConvertible {
@@ -896,6 +997,32 @@ extension Where: CustomDebugStringConvertible, CoreStoreDebugStringConvertible {
}
// MARK: - XcodeDataModel
extension XcodeDataModel: CustomDebugStringConvertible, CoreStoreDebugStringConvertible {
// MARK: CustomDebugStringConvertible
public var debugDescription: String {
return formattedDebugDescription(self)
}
// MARK: CoreStoreDebugStringConvertible
public var coreStoreDumpString: String {
return createFormattedString(
"(", ")",
("modelVersion", self.modelVersion),
("modelVersionFileURL", self.modelVersionFileURL),
("rawModel", self.rawModel())
)
}
}
// MARK: - Private: Utilities
private typealias DumpInfo = [(key: String, value: Any)]
@@ -972,7 +1099,7 @@ public protocol CoreStoreDebugStringConvertible {
}
// MARK: - Private:
// MARK: - Standard Types:
extension Array: CoreStoreDebugStringConvertible {
@@ -1214,7 +1341,15 @@ extension NSSortDescriptor: CoreStoreDebugStringConvertible {
}
}
extension URL: CoreStoreDebugStringConvertible {
extension NSString: CoreStoreDebugStringConvertible {
public var coreStoreDumpString: String {
return "\"\(self)\""
}
}
extension NSURL: CoreStoreDebugStringConvertible {
public var coreStoreDumpString: String {
@@ -1249,3 +1384,11 @@ extension String: CoreStoreDebugStringConvertible {
return "\"\(self)\""
}
}
extension URL: CoreStoreDebugStringConvertible {
public var coreStoreDumpString: String {
return "\"\(self)\""
}
}

View File

@@ -218,7 +218,7 @@ public extension DataStack {
try storage.eraseStorageAndWait(
metadata: metadata,
soureModelHint: self.model[metadata]
soureModelHint: self.schemaHistory.schema(for: metadata)?.rawModel()
)
_ = try self.addStorageAndWait(storage)
@@ -382,7 +382,9 @@ public extension DataStack {
at: cacheFileURL,
options: storeOptions
)
_ = try self.model[metadata].flatMap(storage.eraseStorageAndWait)
_ = try self.schemaHistory
.schema(for: metadata)
.flatMap({ try storage.eraseStorageAndWait(soureModel: $0.rawModel()) })
_ = try self.createPersistentStoreFromStorage(
storage,
finalURL: cacheFileURL,
@@ -497,7 +499,7 @@ public extension DataStack {
let error = CoreStoreError.mappingModelNotFound(
localStoreURL: fileURL,
targetModel: self.model,
targetModel: self.schemaHistory.rawModel,
targetModelVersion: self.modelVersion
)
CoreStore.log(
@@ -545,12 +547,12 @@ public extension DataStack {
let error = CoreStoreError.mappingModelNotFound(
localStoreURL: storage.fileURL,
targetModel: self.model,
targetModel: self.schemaHistory.rawModel,
targetModelVersion: self.modelVersion
)
CoreStore.log(
error,
"Failed to find migration steps from \(cs_typeName(storage)) at URL \"\(storage.fileURL)\" to version model \"\(self.model)\"."
"Failed to find migration steps from \(cs_typeName(storage)) at URL \"\(storage.fileURL)\" to version model \"\(self.schemaHistory.rawModel)\"."
)
DispatchQueue.main.async {
@@ -592,7 +594,7 @@ public extension DataStack {
let progress = Progress(parent: nil, userInfo: nil)
progress.totalUnitCount = numberOfMigrations
for (sourceModel, destinationModel, mappingModel, _) in migrationSteps {
for (sourceModel, destinationModel, mappingModel, migrationType) in migrationSteps {
progress.becomeCurrent(withPendingUnitCount: 1)
@@ -620,8 +622,13 @@ public extension DataStack {
)
}
catch {
migrationResult = MigrationResult(error)
let migrationError = CoreStoreError(error)
CoreStore.log(
migrationError,
"Failed to migrate version model \"\(migrationType.sourceVersion)\" to version \"\(migrationType.destinationVersion)\"."
)
migrationResult = MigrationResult(migrationError)
cancelled = true
}
}
@@ -658,28 +665,27 @@ public extension DataStack {
private func computeMigrationFromStorage<T: LocalStorage>(_ storage: T, metadata: [String: Any]) -> [(sourceModel: NSManagedObjectModel, destinationModel: NSManagedObjectModel, mappingModel: NSMappingModel, migrationType: MigrationType)]? {
let model = self.model
if model.isConfiguration(withName: storage.configuration, compatibleWithStoreMetadata: metadata) {
let schemaHistory = self.schemaHistory
if schemaHistory.rawModel.isConfiguration(withName: storage.configuration, compatibleWithStoreMetadata: metadata) {
return []
}
guard let initialModel = model[metadata],
var currentVersion = initialModel.currentModelVersion else {
return nil
guard let initialSchema = schemaHistory.schema(for: metadata) else {
return nil
}
let migrationChain: MigrationChain = self.migrationChain.empty
? [currentVersion: model.currentModelVersion!]
: self.migrationChain
var currentVersion = initialSchema.modelVersion
let migrationChain: MigrationChain = schemaHistory.migrationChain.isEmpty
? [currentVersion: schemaHistory.currentModelVersion]
: schemaHistory.migrationChain
var migrationSteps = [(sourceModel: NSManagedObjectModel, destinationModel: NSManagedObjectModel, mappingModel: NSMappingModel, migrationType: MigrationType)]()
while let nextVersion = migrationChain.nextVersionFrom(currentVersion),
let sourceModel = model[currentVersion],
sourceModel != model,
let destinationModel = model[nextVersion] {
let sourceModel = schemaHistory.rawModel(for: currentVersion),
sourceModel != schemaHistory.rawModel,
let destinationModel = schemaHistory.rawModel(for: nextVersion) {
if let mappingModel = NSMappingModel(
from: storage.mappingModelBundles,
@@ -727,7 +733,7 @@ public extension DataStack {
currentVersion = nextVersion
}
if migrationSteps.last?.destinationModel == model {
if migrationSteps.last?.destinationModel == schemaHistory.rawModel {
return migrationSteps
}
@@ -775,21 +781,8 @@ public extension DataStack {
}
catch {
do {
try fileManager.removeItem(at: temporaryFileURL)
}
catch _ { }
let sourceVersion = migrationManager.sourceModel.currentModelVersion ?? "???"
let destinationVersion = migrationManager.destinationModel.currentModelVersion ?? "???"
let migrationError = CoreStoreError(error)
CoreStore.log(
migrationError,
"Failed to migrate from version model \"\(sourceVersion)\" to version model \"\(destinationVersion)\"."
)
throw migrationError
_ = try? fileManager.removeItem(at: temporaryFileURL)
throw CoreStoreError(error)
}
do {
@@ -806,21 +799,8 @@ public extension DataStack {
}
catch {
do {
try fileManager.removeItem(at: temporaryFileURL)
}
catch _ { }
let sourceVersion = migrationManager.sourceModel.currentModelVersion ?? "???"
let destinationVersion = migrationManager.destinationModel.currentModelVersion ?? "???"
let fileError = CoreStoreError(error)
CoreStore.log(
fileError,
"Failed to save store after migrating from version model \"\(sourceVersion)\" to version model \"\(destinationVersion)\"."
)
throw fileError
_ = try? fileManager.removeItem(at: temporaryFileURL)
throw CoreStoreError(error)
}
}
}

View File

@@ -70,7 +70,7 @@ public struct MigrationChain: ExpressibleByNilLiteral, ExpressibleByStringLitera
self.versionTree = [:]
self.rootVersions = []
self.leafVersions = []
self.valid = true
self.isValid = true
}
/**
@@ -81,7 +81,7 @@ public struct MigrationChain: ExpressibleByNilLiteral, ExpressibleByStringLitera
self.versionTree = [:]
self.rootVersions = [value]
self.leafVersions = [value]
self.valid = true
self.isValid = true
}
/**
@@ -93,13 +93,13 @@ public struct MigrationChain: ExpressibleByNilLiteral, ExpressibleByStringLitera
var lastVersion: String?
var versionTree = [String: String]()
var valid = true
var isValid = true
for version in elements {
if let lastVersion = lastVersion,
let _ = versionTree.updateValue(version, forKey: lastVersion) {
valid = false
isValid = false
}
lastVersion = version
}
@@ -107,7 +107,7 @@ public struct MigrationChain: ExpressibleByNilLiteral, ExpressibleByStringLitera
self.versionTree = versionTree
self.rootVersions = Set(elements.prefix(1))
self.leafVersions = Set(elements.suffix(1))
self.valid = valid
self.isValid = isValid
}
/**
@@ -115,7 +115,7 @@ public struct MigrationChain: ExpressibleByNilLiteral, ExpressibleByStringLitera
*/
public init(_ elements: [(String, String)]) {
var valid = true
var isValid = true
var versionTree = [String: String]()
elements.forEach { (sourceVersion, destinationVersion) in
@@ -126,7 +126,7 @@ public struct MigrationChain: ExpressibleByNilLiteral, ExpressibleByStringLitera
CoreStore.assert(false, "\(cs_typeName(MigrationChain.self))'s migration chain could not be created due to ambiguous version paths.")
valid = false
isValid = false
}
let leafVersions = Set(
elements
@@ -156,7 +156,7 @@ public struct MigrationChain: ExpressibleByNilLiteral, ExpressibleByStringLitera
self.versionTree = versionTree
self.rootVersions = Set(versionTree.keys).subtracting(versionTree.values)
self.leafVersions = leafVersions
self.valid = valid && Set(versionTree.keys).union(versionTree.values).filter { isVersionAmbiguous($0) }.count <= 0
self.isValid = isValid && Set(versionTree.keys).union(versionTree.values).filter { isVersionAmbiguous($0) }.count <= 0
}
/**
@@ -223,7 +223,7 @@ public struct MigrationChain: ExpressibleByNilLiteral, ExpressibleByStringLitera
return lhs.versionTree == rhs.versionTree
&& lhs.rootVersions == rhs.rootVersions
&& lhs.leafVersions == rhs.leafVersions
&& lhs.valid == rhs.valid
&& lhs.isValid == rhs.isValid
}
@@ -231,9 +231,9 @@ public struct MigrationChain: ExpressibleByNilLiteral, ExpressibleByStringLitera
internal let rootVersions: Set<String>
internal let leafVersions: Set<String>
internal let valid: Bool
internal let isValid: Bool
internal var empty: Bool {
internal var isEmpty: Bool {
return self.versionTree.count <= 0
}

View File

@@ -54,7 +54,7 @@ public final class CSDataStack: NSObject, CoreStoreObjectiveCType {
- parameter versionChain: the version strings that indicate the sequence of model versions to be used as the order for progressive migrations. If not specified, will default to a non-migrating data stack.
*/
@objc
public convenience init(modelName: XcdatamodelFilename?, bundle: Bundle?, versionChain: [String]?) {
public convenience init(modelName: XcodeDataModelFileName?, bundle: Bundle?, versionChain: [String]?) {
self.init(
DataStack(
@@ -73,7 +73,7 @@ public final class CSDataStack: NSObject, CoreStoreObjectiveCType {
- parameter versionTree: the version strings that indicate the sequence of model versions to be used as the order for progressive migrations. If not specified, will default to a non-migrating data stack.
*/
@objc
public convenience init(modelName: XcdatamodelFilename?, bundle: Bundle?, versionTree: [String: String]?) {
public convenience init(modelName: XcodeDataModelFileName?, bundle: Bundle?, versionTree: [String: String]?) {
self.init(
DataStack(
@@ -84,40 +84,6 @@ public final class CSDataStack: NSObject, CoreStoreObjectiveCType {
)
}
/**
Initializes a `DataStack` from an `NSManagedObjectModel`.
- parameter model: the `NSManagedObjectModel` for the stack
- parameter versionChain: the `MigrationChain` that indicates the sequence of model versions to be used as the order for progressive migrations. If not specified, will default to a non-migrating data stack.
*/
@objc
public convenience init(model: NSManagedObjectModel, versionChain: [String]?) {
self.init(
DataStack(
model: model,
migrationChain: versionChain.flatMap { MigrationChain($0) } ?? nil
)
)
}
/**
Initializes a `DataStack` from an `NSManagedObjectModel`.
- parameter model: the `NSManagedObjectModel` for the stack
- parameter versionTree: the `MigrationChain` that indicates the sequence of model versions to be used as the order for progressive migrations. If not specified, will default to a non-migrating data stack.
*/
@objc
public convenience init(model: NSManagedObjectModel, versionTree: [String]?) {
self.init(
DataStack(
model: model,
migrationChain: versionTree.flatMap { MigrationChain($0) } ?? nil
)
)
}
/**
Returns the stack's model version. The version string is the same as the name of the version-specific .xcdatamodeld file.
*/
@@ -261,6 +227,42 @@ public final class CSDataStack: NSObject, CoreStoreObjectiveCType {
// MARK: Deprecated
/**
Initializes a `DataStack` from an `NSManagedObjectModel`.
- parameter model: the `NSManagedObjectModel` for the stack
- parameter versionChain: the `MigrationChain` that indicates the sequence of model versions to be used as the order for progressive migrations. If not specified, will default to a non-migrating data stack.
*/
@available(*, deprecated: 3.1, message: "Use the -[initWithModelName:bundle:versionChain:] initializer.")
@objc
public convenience init(model: NSManagedObjectModel, versionChain: [String]?) {
self.init(
DataStack(
model: model,
migrationChain: versionChain.flatMap { MigrationChain($0) } ?? nil
)
)
}
/**
Initializes a `DataStack` from an `NSManagedObjectModel`.
- parameter model: the `NSManagedObjectModel` for the stack
- parameter versionTree: the `MigrationChain` that indicates the sequence of model versions to be used as the order for progressive migrations. If not specified, will default to a non-migrating data stack.
*/
@available(*, deprecated: 3.1, message: "Use the -[initWithModelName:bundle:versionTree:] initializer.")
@objc
public convenience init(model: NSManagedObjectModel, versionTree: [String]?) {
self.init(
DataStack(
model: model,
migrationChain: versionTree.flatMap { MigrationChain($0) } ?? nil
)
)
}
/**
Returns the entity name-to-class type mapping from the stack's model.
*/

View File

@@ -41,30 +41,24 @@ public final class DataStack: Equatable {
- parameter bundle: an optional bundle to load models from. If not specified, the main bundle will be used.
- parameter migrationChain: the `MigrationChain` that indicates the sequence of model versions to be used as the order for progressive migrations. If not specified, will default to a non-migrating data stack.
*/
public convenience init(modelName: XcdatamodelFilename = DataStack.applicationName, bundle: Bundle = Bundle.main, migrationChain: MigrationChain = nil) {
public convenience init(modelName: XcodeDataModelFileName = DataStack.applicationName, bundle: Bundle = Bundle.main, migrationChain: MigrationChain = nil) {
let model = NSManagedObjectModel.fromBundle(
bundle,
modelName: modelName,
modelVersionHints: migrationChain.leafVersions
)
self.init(model: model, migrationChain: migrationChain)
}
public convenience init(dynamicModel: DynamicModel) {
self.init(model: dynamicModel.createModel())
}
public convenience init(dynamicModels: [DynamicModel], migrationChain: MigrationChain = nil) {
CoreStore.assert(
migrationChain.valid,
"Invalid migration chain passed to the \(cs_typeName(DataStack.self)). Check that the model versions' order is correct and that no repetitions or ambiguities exist."
)
self.init(
model: NSManagedObjectModel(byMerging: dynamicModels.map({ $0.createModel() }))!,
migrationChain: migrationChain
schemaHistory: SchemaHistory(
modelName: modelName,
bundle: bundle,
migrationChain: migrationChain
)
)
}
public convenience init(_ schema: DynamicSchema, _ otherSchema: DynamicSchema..., migrationChain: MigrationChain = nil) {
self.init(
schemaHistory: SchemaHistory(
allSchema: [schema] + otherSchema,
migrationChain: migrationChain
)
)
}
@@ -74,21 +68,20 @@ public final class DataStack: Equatable {
- parameter model: the `NSManagedObjectModel` for the stack
- parameter migrationChain: the `MigrationChain` that indicates the sequence of model versions to be used as the order for progressive migrations. If not specified, will default to a non-migrating data stack.
*/
public required init(model: NSManagedObjectModel, migrationChain: MigrationChain = nil) {
public required init(schemaHistory: SchemaHistory) {
// TODO: test before release (rolled back)
// _ = DataStack.isGloballyInitialized
CoreStore.assert(
migrationChain.valid,
schemaHistory.migrationChain.isValid,
"Invalid migration chain passed to the \(cs_typeName(DataStack.self)). Check that the model versions' order is correct and that no repetitions or ambiguities exist."
)
self.coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
self.coordinator = NSPersistentStoreCoordinator(managedObjectModel: schemaHistory.rawModel)
self.rootSavingContext = NSManagedObjectContext.rootSavingContextForCoordinator(self.coordinator)
self.mainContext = NSManagedObjectContext.mainContextForRootContext(self.rootSavingContext)
self.model = model
self.migrationChain = migrationChain
self.schemaHistory = schemaHistory
self.rootSavingContext.parentStack = self
@@ -100,7 +93,7 @@ public final class DataStack: Equatable {
*/
public var modelVersion: String {
return self.model.currentModelVersion!
return self.schemaHistory.currentModelVersion
}
/**
@@ -109,7 +102,7 @@ public final class DataStack: Equatable {
public func entityTypesByName(for type: NSManagedObject.Type) -> [EntityName: NSManagedObject.Type] {
var entityTypesByName: [EntityName: NSManagedObject.Type] = [:]
for (entityIdentifier, entityDescription) in self.model.entityDescriptionsByEntityIdentifier {
for (entityIdentifier, entityDescription) in self.schemaHistory.entityDescriptionsByEntityIdentifier {
switch entityIdentifier.category {
@@ -133,7 +126,7 @@ public final class DataStack: Equatable {
public func entityTypesByName(for type: CoreStoreObject.Type) -> [EntityName: CoreStoreObject.Type] {
var entityTypesByName: [EntityName: CoreStoreObject.Type] = [:]
for (entityIdentifier, entityDescription) in self.model.entityDescriptionsByEntityIdentifier {
for (entityIdentifier, entityDescription) in self.schemaHistory.entityDescriptionsByEntityIdentifier {
switch entityIdentifier.category {
@@ -332,7 +325,7 @@ public final class DataStack: Equatable {
)
try storage.eraseStorageAndWait(
metadata: metadata,
soureModelHint: self.model[metadata]
soureModelHint: self.schemaHistory.schema(for: metadata)?.rawModel()
)
let finalStoreOptions = storage.dictionary(forOptions: storage.localStorageOptions)
_ = try self.createPersistentStoreFromStorage(
@@ -425,7 +418,9 @@ public final class DataStack: Equatable {
at: cacheFileURL,
options: storeOptions
)
_ = try self.model[metadata].flatMap(storage.eraseStorageAndWait)
_ = try self.schemaHistory
.schema(for: metadata)
.flatMap({ try storage.eraseStorageAndWait(soureModel: $0.rawModel()) })
_ = try self.createPersistentStoreFromStorage(
storage,
finalURL: cacheFileURL,
@@ -464,11 +459,10 @@ public final class DataStack: Equatable {
internal let coordinator: NSPersistentStoreCoordinator
internal let rootSavingContext: NSManagedObjectContext
internal let mainContext: NSManagedObjectContext
internal let model: NSManagedObjectModel
internal let migrationChain: MigrationChain
internal let schemaHistory: SchemaHistory
internal let childTransactionQueue = DispatchQueue.serial("com.coreStore.dataStack.childTransactionQueue")
internal let storeMetadataUpdateQueue = DispatchQueue.concurrent("com.coreStore.persistentStoreBarrierQueue")
internal let migrationQueue: OperationQueue = {
internal let migrationQueue: OperationQueue = cs_lazy {
let migrationQueue = OperationQueue()
migrationQueue.maxConcurrentOperationCount = 1
@@ -476,7 +470,7 @@ public final class DataStack: Equatable {
migrationQueue.qualityOfService = .utility
migrationQueue.underlyingQueue = DispatchQueue.serial("com.coreStore.migrationQueue", qos: .userInitiated)
return migrationQueue
}()
}
internal func persistentStoreForStorage(_ storage: StorageInterface) -> NSPersistentStore? {
@@ -562,7 +556,7 @@ public final class DataStack: Equatable {
internal func entityDescription(for entityIdentifier: EntityIdentifier) -> NSEntityDescription? {
return self.model.entityDescriptionsByEntityIdentifier[entityIdentifier]
return self.schemaHistory.entityDescriptionsByEntityIdentifier[entityIdentifier]
}
@@ -596,6 +590,30 @@ public final class DataStack: Equatable {
// MARK: Deprecated
/**
Initializes a `DataStack` from an `NSManagedObjectModel`.
- parameter model: the `NSManagedObjectModel` for the stack
- parameter migrationChain: the `MigrationChain` that indicates the sequence of model versions to be used as the order for progressive migrations. If not specified, will default to a non-migrating data stack.
*/
@available(*, deprecated: 3.1, message: "Use the new DataStack.init(schemaHistory:) initializer passing a LegacyXcodeDataModel instance as argument")
public convenience init(model: NSManagedObjectModel, migrationChain: MigrationChain = nil) {
let modelVersion = migrationChain.leafVersions.first!
self.init(
schemaHistory: SchemaHistory(
allSchema: [
LegacyXcodeDataModel(
modelName: modelVersion,
model: model
)
],
migrationChain: migrationChain,
exactCurrentModelVersion: modelVersion
)
)
}
/**
Returns the entity name-to-class type mapping from the `DataStack`'s model.
*/

View File

@@ -1,5 +1,5 @@
//
// DynamicModel.swift
// CoreStoreSchema.swift
// CoreStore
//
// Copyright © 2017 John Rommel Estropia
@@ -23,20 +23,31 @@
// SOFTWARE.
//
import CoreGraphics
import CoreData
import Foundation
// MARK: - DynamicModel
// MARK: - CoreStoreSchema
public final class DynamicModel {
public final class CoreStoreSchema: DynamicSchema {
public convenience init(version: String, entities: [EntityProtocol]) {
public convenience init(modelVersion: String, _ entity: DynamicEntity, _ entities: DynamicEntity...) {
self.init(version: version, entitiesByConfiguration: [DataStack.defaultConfigurationName: entities])
self.init(
modelVersion: modelVersion,
entities: [entity] + entities
)
}
public required init(version: String, entitiesByConfiguration: [String: [EntityProtocol]]) {
public convenience init(modelVersion: String, entities: [DynamicEntity]) {
self.init(
modelVersion: modelVersion,
entitiesByConfiguration: [DataStack.defaultConfigurationName: entities]
)
}
public required init(modelVersion: String, entitiesByConfiguration: [String: [DynamicEntity]]) {
var actualEntitiesByConfiguration: [String: Set<AnyEntity>] = [:]
for (configuration, entities) in entitiesByConfiguration {
@@ -56,19 +67,57 @@ public final class DynamicModel {
"Ambiguous entity types or entity names were found in the model. Ensure that the entity types and entity names are unique to each other. Entities: \(allEntities)"
)
self.version = version
self.modelVersion = modelVersion
self.entitiesByConfiguration = actualEntitiesByConfiguration
self.allEntities = allEntities
}
// MARK: - DynamicSchema
public let modelVersion: ModelVersion
public func rawModel() -> NSManagedObjectModel {
if let cachedRawModel = self.cachedRawModel {
return cachedRawModel
}
let rawModel = NSManagedObjectModel()
var entityDescriptionsByEntity: [AnyEntity: NSEntityDescription] = [:]
for entity in self.allEntities {
let entityDescription = self.entityDescription(
for: entity,
initializer: CoreStoreSchema.firstPassCreateEntityDescription
)
entityDescriptionsByEntity[entity] = (entityDescription.copy() as! NSEntityDescription)
}
CoreStoreSchema.secondPassConnectRelationshipAttributes(for: entityDescriptionsByEntity)
CoreStoreSchema.thirdPassConnectInheritanceTree(for: entityDescriptionsByEntity)
rawModel.entities = entityDescriptionsByEntity.values.sorted(by: { $0.name! < $1.name! })
for (configuration, entities) in self.entitiesByConfiguration {
rawModel.setEntities(
entities
.map({ entityDescriptionsByEntity[$0]! })
.sorted(by: { $0.name! < $1.name! }),
forConfigurationName: configuration
)
}
self.cachedRawModel = rawModel
return rawModel
}
// MARK: Internal
// MARK: - AnyEntity
internal struct AnyEntity: EntityProtocol, Hashable {
internal struct AnyEntity: DynamicEntity, Hashable {
internal init(_ entity: EntityProtocol) {
internal init(_ entity: DynamicEntity) {
self.type = entity.type
self.entityName = entity.entityName
@@ -97,7 +146,7 @@ public final class DynamicModel {
^ self.entityName.hashValue
}
// MARK: EntityProtocol
// MARK: DynamicEntity
internal let type: CoreStoreObject.Type
internal let entityName: EntityName
@@ -106,41 +155,30 @@ public final class DynamicModel {
// MARK: -
internal func createModel() -> NSManagedObjectModel {
internal let entitiesByConfiguration: [String: Set<AnyEntity>]
// MARK: Private
private static let barrierQueue = DispatchQueue.concurrent("com.coreStore.coreStoreDataModelBarrierQueue")
private let allEntities: Set<AnyEntity>
private var entityDescriptionsByEntity: [CoreStoreSchema.AnyEntity: NSEntityDescription] = [:]
private weak var cachedRawModel: NSManagedObjectModel?
private func entityDescription(for entity: CoreStoreSchema.AnyEntity, initializer: (CoreStoreSchema.AnyEntity) -> NSEntityDescription) -> NSEntityDescription {
let model = NSManagedObjectModel()
let entityDescriptionsByEntity: [AnyEntity: NSEntityDescription] = ModelCache.performUpdate {
if let cachedEntityDescription = self.entityDescriptionsByEntity[entity] {
var entityDescriptionsByEntity: [AnyEntity: NSEntityDescription] = [:]
for entity in self.allEntities {
let entityDescription = ModelCache.entityDescription(
for: entity,
initializer: DynamicModel.firstPassCreateEntityDescription
)
entityDescriptionsByEntity[entity] = entityDescription
}
DynamicModel.secondPassConnectRelationshipAttributes(for: entityDescriptionsByEntity)
DynamicModel.thirdPassConnectInheritanceTree(for: entityDescriptionsByEntity)
return entityDescriptionsByEntity
return cachedEntityDescription
}
model.entities = entityDescriptionsByEntity.values.sorted(by: { $0.name! < $1.name! })
for (configuration, entities) in self.entitiesByConfiguration {
model.setEntities(
entities
.map({ entityDescriptionsByEntity[$0]! })
.sorted(by: { $0.name! < $1.name! }),
forConfigurationName: configuration
)
}
return model
let entityDescription = withoutActuallyEscaping(initializer, do: { $0(entity) })
self.entityDescriptionsByEntity[entity] = entityDescription
return entityDescription
}
// MARK: FilePrivate
fileprivate static func firstPassCreateEntityDescription(from entity: AnyEntity) -> NSEntityDescription {
private static func firstPassCreateEntityDescription(from entity: AnyEntity) -> NSEntityDescription {
let entityDescription = NSEntityDescription()
entityDescription.anyEntity = entity
@@ -186,7 +224,7 @@ public final class DynamicModel {
return entityDescription
}
fileprivate static func secondPassConnectRelationshipAttributes(for entityDescriptionsByEntity: [AnyEntity: NSEntityDescription]) {
private static func secondPassConnectRelationshipAttributes(for entityDescriptionsByEntity: [AnyEntity: NSEntityDescription]) {
var relationshipsByNameByEntity: [AnyEntity: [String: NSRelationshipDescription]] = [:]
for (entity, entityDescription) in entityDescriptionsByEntity {
@@ -207,7 +245,7 @@ public final class DynamicModel {
if matchedEntities.isEmpty {
CoreStore.abort(
"No \(cs_typeName("Entity<\(type)>")) instance found in the \(cs_typeName(DynamicModel.self))."
"No \(cs_typeName("Entity<\(type)>")) instance found in the \(cs_typeName(CoreStoreSchema.self))."
)
}
else {
@@ -277,7 +315,7 @@ public final class DynamicModel {
}
}
fileprivate static func thirdPassConnectInheritanceTree(for entityDescriptionsByEntity: [AnyEntity: NSEntityDescription]) {
private static func thirdPassConnectInheritanceTree(for entityDescriptionsByEntity: [AnyEntity: NSEntityDescription]) {
func connectBaseEntity(mirror: Mirror, entityDescription: NSEntityDescription) {
@@ -304,40 +342,4 @@ public final class DynamicModel {
)
}
}
// MARK: Private
private let version: String
private let allEntities: Set<AnyEntity>
private let entitiesByConfiguration: [String: Set<AnyEntity>]
}
// MARK: - ModelCache
fileprivate enum ModelCache {
fileprivate static func performUpdate<T>(_ closure: () -> T) -> T {
return self.barrierQueue.cs_barrierSync(closure)
}
fileprivate static func entityDescription(for entity: DynamicModel.AnyEntity, initializer: (DynamicModel.AnyEntity) -> NSEntityDescription) -> NSEntityDescription {
if let cachedEntityDescription = self.entityDescriptionsByEntity[entity] {
return cachedEntityDescription
}
let entityDescription = withoutActuallyEscaping(initializer, do: { $0(entity) })
self.entityDescriptionsByEntity[entity] = entityDescription
return entityDescription
}
// MARK: Private
private static let barrierQueue = DispatchQueue.concurrent("com.coreStore.modelCacheBarrierQueue")
private static var entityDescriptionsByEntity: [DynamicModel.AnyEntity: NSEntityDescription] = [:]
}

View File

@@ -0,0 +1,37 @@
//
// DynamicSchema.swift
// CoreStore
//
// Copyright © 2017 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import CoreData
import Foundation
// MARK: - DynamicSchema
public protocol DynamicSchema {
var modelVersion: ModelVersion { get }
func rawModel() -> NSManagedObjectModel
}

View File

@@ -0,0 +1,54 @@
//
// LegacyXcodeDataModel.swift
// CoreStore
//
// Copyright © 2017 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import CoreData
import Foundation
// MARK: - LegacyXcodeDataModel
public final class LegacyXcodeDataModel: DynamicSchema {
public required init(modelName: ModelVersion, model: NSManagedObjectModel) {
self.modelVersion = modelName
self.model = model
}
// MARK: DynamicSchema
public let modelVersion: ModelVersion
public func rawModel() -> NSManagedObjectModel {
return self.model
}
// MARK: Private
private let model: NSManagedObjectModel
}

View File

@@ -0,0 +1,78 @@
//
// XcodeDataModel.swift
// CoreStore
//
// Copyright © 2017 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import CoreData
import Foundation
// MARK: - XcodeDataModel
public final class XcodeDataModel: DynamicSchema {
public required init(modelVersion: ModelVersion, modelVersionFileURL: URL) {
CoreStore.assert(
NSManagedObjectModel(contentsOf: modelVersionFileURL) != nil,
"Could not find the \"\(modelVersion).mom\" version file for the model at URL \"\(modelVersionFileURL)\"."
)
self.modelVersion = modelVersion
self.modelVersionFileURL = modelVersionFileURL
}
// MARK: DynamicSchema
public let modelVersion: ModelVersion
public func rawModel() -> NSManagedObjectModel {
if let cachedRawModel = self.cachedRawModel {
return cachedRawModel
}
if let rawModel = NSManagedObjectModel(contentsOf: self.modelVersionFileURL) {
self.cachedRawModel = rawModel
return rawModel
}
CoreStore.abort("Could not create an \(cs_typeName(NSManagedObjectModel.self)) from the model at URL \"\(self.modelVersionFileURL)\".")
}
// MARK: Internal
internal let modelVersionFileURL: URL
private lazy var rootModelFileURL: URL = cs_lazy { [unowned self] in
return self.modelVersionFileURL.deletingLastPathComponent()
}
// MARK: Private
private weak var cachedRawModel: NSManagedObjectModel?
}

View File

@@ -28,9 +28,9 @@ import Foundation
import ObjectiveC
// MARK: - EntityProtocol
// MARK: - DynamicEntity
public protocol EntityProtocol {
public protocol DynamicEntity {
var type: CoreStoreObject.Type { get }
var entityName: EntityName { get }
@@ -39,7 +39,7 @@ public protocol EntityProtocol {
// MARK: Entity
public struct Entity<O: CoreStoreObject>: EntityProtocol {
public struct Entity<O: CoreStoreObject>: DynamicEntity, Hashable {
public init(_ entityName: String) {
@@ -47,136 +47,54 @@ public struct Entity<O: CoreStoreObject>: EntityProtocol {
self.entityName = entityName
}
// MARK: EntityProtocol
public init(_ type: O.Type, _ entityName: String) {
self.type = type
self.entityName = entityName
}
// MARK: - VersionHash
public struct VersionHash: ExpressibleByArrayLiteral {
let hash: Data
public init(_ hash: Data) {
self.hash = hash
}
// MARK: ExpressibleByArrayLiteral
public typealias Element = UInt8
public init(arrayLiteral elements: UInt8...) {
self.hash = Data(bytes: elements)
}
}
// MARK: DynamicEntity
public let type: CoreStoreObject.Type
public let entityName: EntityName
}
// MARK: - EntityIdentifier
internal struct EntityIdentifier: Hashable {
// MARK: - Category
internal enum Category: Int {
case coreData
case coreStore
}
// MARK: -
internal let category: Category
internal let interfacedClassName: String
internal init(_ type: NSManagedObject.Type) {
self.category = .coreData
self.interfacedClassName = String(reflecting: type)
}
internal init(_ type: CoreStoreObject.Type) {
self.category = .coreStore
self.interfacedClassName = String(reflecting: type)
}
internal init(_ type: DynamicObject.Type) {
switch type {
case let type as NSManagedObject.Type:
self.init(type)
case let type as CoreStoreObject.Type:
self.init(type)
default:
CoreStore.abort("\(cs_typeName(DynamicObject.self)) is not meant to be implemented by external types.")
}
}
internal init(_ entityDescription: NSEntityDescription) {
if let entity = entityDescription.anyEntity {
self.category = .coreStore
self.interfacedClassName = NSStringFromClass(entity.type)
}
else {
self.category = .coreData
self.interfacedClassName = entityDescription.managedObjectClassName!
}
}
// MARK: Equatable
static func == (lhs: EntityIdentifier, rhs: EntityIdentifier) -> Bool {
public static func == (lhs: Entity, rhs: Entity) -> Bool {
return lhs.category == rhs.category
&& lhs.interfacedClassName == rhs.interfacedClassName
return lhs.type == rhs.type
&& lhs.entityName == rhs.entityName
}
// MARK: Hashable
var hashValue: Int {
return self.category.hashValue
^ self.interfacedClassName.hashValue
}
}
// MARK: - NSEntityDescription
internal extension NSEntityDescription {
@nonobjc
internal var anyEntity: DynamicModel.AnyEntity? {
get {
guard let userInfo = self.userInfo,
let typeName = userInfo[UserInfoKey.CoreStoreManagedObjectTypeName] as! String?,
let entityName = userInfo[UserInfoKey.CoreStoreManagedObjectEntityName] as! String? else {
return nil
}
return DynamicModel.AnyEntity(
type: NSClassFromString(typeName) as! CoreStoreObject.Type,
entityName: entityName
)
}
set {
if let newValue = newValue {
self.userInfo = [
UserInfoKey.CoreStoreManagedObjectTypeName: NSStringFromClass(newValue.type),
UserInfoKey.CoreStoreManagedObjectEntityName: newValue.entityName
]
}
else {
self.userInfo = [:]
}
}
}
// MARK: Private
// MARK: - UserInfoKey
fileprivate enum UserInfoKey {
fileprivate static let CoreStoreManagedObjectTypeName = "CoreStoreManagedObjectTypeName"
fileprivate static let CoreStoreManagedObjectEntityName = "CoreStoreManagedObjectEntityName"
public var hashValue: Int {
return ObjectIdentifier(self.type).hashValue
}
}

View File

@@ -0,0 +1,227 @@
//
// SchemaHistory.swift
// CoreStore
//
// Copyright © 2017 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import CoreData
import Foundation
// MARK: - SchemaHistory
public final class SchemaHistory: ExpressibleByArrayLiteral {
public let currentModelVersion: ModelVersion
public let migrationChain: MigrationChain
public convenience init(modelName: XcodeDataModelFileName, bundle: Bundle = Bundle.main, migrationChain: MigrationChain = nil) {
guard let modelFilePath = bundle.path(forResource: modelName, ofType: "momd") else {
// For users migrating from very old Xcode versions: Old xcdatamodel files are not contained inside xcdatamodeld (with a "d"), and will thus fail this check. If that was the case, create a new xcdatamodeld file and copy all contents into the new model.
let foundModels = bundle
.paths(forResourcesOfType: "momd", inDirectory: nil)
.map({ ($0 as NSString).lastPathComponent })
CoreStore.abort("Could not find \"\(modelName).momd\" from the bundle \"\(bundle.bundleIdentifier ?? "<nil>")\". Other model files in bundle: \(foundModels.coreStoreDumpString)")
}
let modelFileURL = URL(fileURLWithPath: modelFilePath)
let versionInfoPlistURL = modelFileURL.appendingPathComponent("VersionInfo.plist", isDirectory: false)
guard let versionInfo = NSDictionary(contentsOf: versionInfoPlistURL),
let versionHashes = versionInfo["NSManagedObjectModel_VersionHashes"] as? [String: AnyObject] else {
CoreStore.abort("Could not load \(cs_typeName(NSManagedObjectModel.self)) metadata from path \"\(versionInfoPlistURL)\".")
}
let modelVersions = Set(versionHashes.keys)
let modelVersionHints = migrationChain.leafVersions
let currentModelVersion: String
if let plistModelVersion = versionInfo["NSManagedObjectModel_CurrentVersionName"] as? String,
modelVersionHints.isEmpty || modelVersionHints.contains(plistModelVersion) {
currentModelVersion = plistModelVersion
}
else if let resolvedVersion = modelVersions.intersection(modelVersionHints).first {
CoreStore.log(
.warning,
message: "The \(cs_typeName(MigrationChain.self)) leaf versions do not include the model file's current version. Resolving to version \"\(resolvedVersion)\"."
)
currentModelVersion = resolvedVersion
}
else if let resolvedVersion = modelVersions.first ?? modelVersionHints.first {
if !modelVersionHints.isEmpty {
CoreStore.log(
.warning,
message: "The \(cs_typeName(MigrationChain.self)) leaf versions do not include any of the model file's embedded versions. Resolving to version \"\(resolvedVersion)\"."
)
}
currentModelVersion = resolvedVersion
}
else {
CoreStore.abort("No model files were found in URL \"\(modelFileURL)\".")
}
var allSchema: [DynamicSchema] = []
for modelVersion in modelVersions {
let fileURL = modelFileURL.appendingPathComponent("\(modelVersion).mom", isDirectory: false)
allSchema.append(XcodeDataModel(modelVersion: modelVersion, modelVersionFileURL: fileURL))
}
self.init(
allSchema: allSchema,
migrationChain: migrationChain,
exactCurrentModelVersion: currentModelVersion
)
}
public convenience init(_ schema: DynamicSchema, _ otherSchema: DynamicSchema..., migrationChain: MigrationChain = nil, exactCurrentModelVersion: String? = nil) {
self.init(
allSchema: [schema] + otherSchema,
migrationChain: migrationChain,
exactCurrentModelVersion: exactCurrentModelVersion
)
}
public required init(allSchema: [DynamicSchema], migrationChain: MigrationChain = nil, exactCurrentModelVersion: String? = nil) {
if allSchema.isEmpty {
CoreStore.abort("The \"allSchema\" argument of the \(cs_typeName(SchemaHistory.self)) initializer cannot be empty.")
}
var schemaByVersion: [ModelVersion: DynamicSchema] = [:]
for schema in allSchema {
let modelVersion = schema.modelVersion
CoreStore.assert(
schemaByVersion[modelVersion] == nil,
"Multiple model schema found for model version \"\(modelVersion)\"."
)
schemaByVersion[modelVersion] = schema
}
let modelVersions = Set(schemaByVersion.keys)
let currentModelVersion: ModelVersion
if let exactCurrentModelVersion = exactCurrentModelVersion {
if !migrationChain.isEmpty && !migrationChain.contains(exactCurrentModelVersion) {
CoreStore.abort("An \"exactCurrentModelVersion\" argument was provided to \(cs_typeName(SchemaHistory.self)) initializer but a matching schema could not be found from the provided \(cs_typeName(MigrationChain.self)).")
}
if schemaByVersion[exactCurrentModelVersion] == nil {
CoreStore.abort("An \"exactCurrentModelVersion\" argument was provided to \(cs_typeName(SchemaHistory.self)) initializer but a matching schema could not be found from the \(cs_typeName(DynamicSchema.self)) list.")
}
currentModelVersion = exactCurrentModelVersion
}
else if migrationChain.isEmpty && schemaByVersion.count == 1 {
currentModelVersion = schemaByVersion.keys.first!
}
else {
let candidateVersions = modelVersions.intersection(migrationChain.leafVersions)
switch candidateVersions.count {
case 0:
CoreStore.abort("None of the \(cs_typeName(MigrationChain.self)) leaf versions provided to the \(cs_typeName(SchemaHistory.self)) initializer matches any scheme from the \(cs_typeName(DynamicSchema.self)) list.")
case 1:
currentModelVersion = candidateVersions.first!
default:
CoreStore.abort("Could not resolve the \(cs_typeName(SchemaHistory.self)) current model version because the \(cs_typeName(MigrationChain.self)) have ambiguous leaf versions: \(candidateVersions)")
}
}
self.schemaByVersion = schemaByVersion
self.migrationChain = migrationChain
self.currentModelVersion = currentModelVersion
self.rawModel = schemaByVersion[currentModelVersion]!.rawModel()
}
// MARK: ExpressibleByArrayLiteral
public typealias Element = DynamicSchema
public convenience init(arrayLiteral elements: DynamicSchema...) {
self.init(
allSchema: elements,
migrationChain: MigrationChain(elements.map({ $0.modelVersion })),
exactCurrentModelVersion: nil
)
}
// MARK: Internal
internal let schemaByVersion: [ModelVersion: DynamicSchema]
internal let rawModel: NSManagedObjectModel
internal private(set) lazy var entityDescriptionsByEntityIdentifier: [EntityIdentifier: NSEntityDescription] = cs_lazy { [unowned self] in
var mapping: [EntityIdentifier: NSEntityDescription] = [:]
self.rawModel.entities.forEach { (entityDescription) in
let entityIdentifier = EntityIdentifier(entityDescription)
mapping[entityIdentifier] = entityDescription
}
return mapping
}
internal func rawModel(for modelVersion: ModelVersion) -> NSManagedObjectModel? {
if modelVersion == self.currentModelVersion {
return self.rawModel
}
return self.schemaByVersion[modelVersion]?.rawModel()
}
internal func schema(for storeMetadata: [String: Any]) -> DynamicSchema? {
guard let modelHashes = storeMetadata[NSStoreModelVersionHashesKey] as! [String: Data]? else {
return nil
}
for (_, schema) in self.schemaByVersion {
let rawModel = schema.rawModel()
if modelHashes == rawModel.entityVersionHashesByName {
return schema
}
}
return nil
}
internal func mergedModels() -> [NSManagedObjectModel] {
return self.schemaByVersion.values.map({ $0.rawModel() })
}
}

View File

@@ -233,7 +233,7 @@ public final class LegacySQLiteStore: LocalStorage, DefaultInitializableStore {
// MARK: Internal
internal static let defaultRootDirectory: URL = {
internal static let defaultRootDirectory: URL = cs_lazy {
#if os(tvOS)
let systemDirectorySearchPath = FileManager.SearchPathDirectory.cachesDirectory
@@ -244,7 +244,7 @@ public final class LegacySQLiteStore: LocalStorage, DefaultInitializableStore {
return FileManager.default.urls(
for: systemDirectorySearchPath,
in: .userDomainMask).first!
}()
}
internal static let defaultFileURL = LegacySQLiteStore.defaultRootDirectory
.appendingPathComponent(DataStack.applicationName, isDirectory: false)

View File

@@ -230,7 +230,7 @@ public final class SQLiteStore: LocalStorage, DefaultInitializableStore {
// MARK: Internal
internal static let defaultRootDirectory: URL = {
internal static let defaultRootDirectory: URL = cs_lazy {
#if os(tvOS)
let systemDirectorySearchPath = FileManager.SearchPathDirectory.cachesDirectory
@@ -246,7 +246,7 @@ public final class SQLiteStore: LocalStorage, DefaultInitializableStore {
Bundle.main.bundleIdentifier ?? "com.CoreStore.DataStack",
isDirectory: true
)
}()
}
internal static let defaultFileURL = SQLiteStore.defaultRootDirectory
.appendingPathComponent(