Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 183 additions & 10 deletions DataKernel.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0720"
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0720"
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
2 changes: 1 addition & 1 deletion DataKernel/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?


func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
Expand Down
6 changes: 6 additions & 0 deletions DataKernel/Classes/Contracts/DKStoreLoader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Foundation
import CoreData

public protocol DKStoreLoader {
func append(store: URL, ofType: String, to coordinator: NSPersistentStoreCoordinator) throws -> NSPersistentStore
}
32 changes: 15 additions & 17 deletions DataKernel/Classes/CoreData/CoreDataLocalStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ open class CoreDataLocalStorage: Storage {
// MARK: - Storage

internal let store: StoreRef
internal let migration: Bool

internal let loader: DKStoreLoader
open var uiContext: Context!

open func perform(_ ephemeral: Bool, unitOfWork: @escaping (_ context: Context, _ save: () -> Void) throws -> Void) throws {
Expand Down Expand Up @@ -92,7 +91,7 @@ open class CoreDataLocalStorage: Storage {
}

open func restoreStore() throws {
self.persistentStore = try initializeStore(store, coordinator: self.persistentStoreCoordinator, migrate: self.migration)
self.persistentStore = try initializeStore(store, coordinator: self.persistentStoreCoordinator)
}

// MARK: - Props
Expand All @@ -103,14 +102,16 @@ open class CoreDataLocalStorage: Storage {
internal var rootContext: NSManagedObjectContext! = nil

// MARK: - Init

public init(store: StoreRef, model: ModelRef, migration: Bool) throws {
public convenience init(store: StoreRef, model: ModelRef, migration: Bool) throws {
try self.init(store: store, model: model, loader: DKStandardStoreLoader(migrate: migration))
}

public init(store: StoreRef, model: ModelRef, loader: DKStoreLoader) throws {
self.store = store
self.migration = migration

self.loader = loader
self.model = model.build()!
self.persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.model)
self.persistentStore = try initializeStore(store, coordinator: self.persistentStoreCoordinator, migrate: migration)
self.persistentStore = try initializeStore(store, coordinator: self.persistentStoreCoordinator)
self.rootContext = initializeContext(.coordinator(self.persistentStoreCoordinator), concurrency: .privateQueueConcurrencyType)
self.uiContext = initializeContext(.context(self.rootContext), concurrency: .mainQueueConcurrencyType)
}
Expand Down Expand Up @@ -155,24 +156,23 @@ open class CoreDataLocalStorage: Storage {
return context
}

fileprivate func initializeStore(_ store: StoreRef, coordinator: NSPersistentStoreCoordinator, migrate: Bool) throws -> NSPersistentStore {
fileprivate func initializeStore(_ store: StoreRef, coordinator: NSPersistentStoreCoordinator) throws -> NSPersistentStore {
try checkStorePath(store)
let options = migrate ? OptionRef.migration : OptionRef.default
return try addStore(store, coordinator: coordinator, options: options.build())
return try addStore(store, coordinator: coordinator)
}

fileprivate func checkStorePath(_ store: StoreRef) throws {
let path = store.location().deletingLastPathComponent()
try FileManager.default.createDirectory(at: path, withIntermediateDirectories: true, attributes: nil)
}

fileprivate func addStore(_ store: StoreRef, coordinator: NSPersistentStoreCoordinator, options: [AnyHashable: Any], retry: Bool = true) throws -> NSPersistentStore {
fileprivate func addStore(_ store: StoreRef, coordinator: NSPersistentStoreCoordinator, retry: Bool = true) throws -> NSPersistentStore {
var pstore: NSPersistentStore?
var error: NSError?

let loader = self.loader
coordinator.performAndWait({
do {
pstore = try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: store.location() as URL, options: options)
pstore = try loader.append(store: store.location() as URL, ofType: NSSQLiteStoreType, to: coordinator)
} catch let _error as NSError {
error = _error
}
Expand All @@ -182,7 +182,7 @@ open class CoreDataLocalStorage: Storage {
let errorOnMigration = error.code == NSPersistentStoreIncompatibleVersionHashError || error.code == NSMigrationMissingSourceModelError
if errorOnMigration && retry {
try cleanStoreOnFailedMigration(store)
return try addStore(store, coordinator: coordinator, options: options, retry: false)
return try addStore(store, coordinator: coordinator, retry: false)
} else {
throw error
}
Expand All @@ -201,6 +201,4 @@ open class CoreDataLocalStorage: Storage {
try FileManager.default.removeItem(at: shmSidecar)
try FileManager.default.removeItem(at: walSidecar)
}


}
14 changes: 14 additions & 0 deletions DataKernel/Classes/CoreData/DKStandardStoreLoader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation
import CoreData

public struct DKStandardStoreLoader: DKStoreLoader {
public let migrate: Bool
public func append(store: URL, ofType: String, to coordinator: NSPersistentStoreCoordinator) throws -> NSPersistentStore {
let options = migrate ? OptionRef.migration : OptionRef.default
return try coordinator.addPersistentStore(
ofType: ofType,
configurationName: nil,
at: store,
options: options.build())
}
}
61 changes: 61 additions & 0 deletions DataKernel/Classes/Migration/DKMigration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Foundation
import CoreData

public struct DKMigration {
public let from: NSManagedObjectModel
public let to: NSManagedObjectModel
public let mapping: NSMappingModel?

public func apply(url: URL, sourceStoreType: String, targetStoreType: String) throws {
if self.canPerformLightweightMigration(sourceStoreType: sourceStoreType, targetStoreType: targetStoreType) {
try performLightweightMigration(url: url, storeType: sourceStoreType)
} else {
try performFullMigration(url: url, sourceStoreType: sourceStoreType, targetStoreType: targetStoreType)
}
}

public func canPerformLightweightMigration(sourceStoreType: String, targetStoreType: String) -> Bool {
if sourceStoreType != targetStoreType {
return false
}
guard let mapping = mapping else {
return true
}
for entityMapping in mapping.entityMappings {
if entityMapping.mappingType == .customEntityMappingType {
return false
}
}
return true
}

public func performLightweightMigration(url: URL, storeType: String) throws {
let options: [AnyHashable: Any] = [
NSMigratePersistentStoresAutomaticallyOption: true,
NSInferMappingModelAutomaticallyOption: mapping == nil
]
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: to)
try coordinator.addPersistentStore(
ofType: storeType,
configurationName: nil,
at: url,
options: options)
}

public func performFullMigration(url: URL, sourceStoreType: String, targetStoreType: String) throws {
let tempDir = URL(fileURLWithPath: NSTemporaryDirectory())
let tempUrl = tempDir.appendingPathComponent("\(UUID().uuidString).tmp")
let migrationManager = NSMigrationManager(
sourceModel: from,
destinationModel: to)
try migrationManager.migrateStore(
from: url,
sourceType: sourceStoreType,
options: nil,
with: mapping,
toDestinationURL: tempUrl,
destinationType: targetStoreType,
destinationOptions: nil)
try DKStoreFile(url: tempUrl).move(to: url)
}
}
7 changes: 7 additions & 0 deletions DataKernel/Classes/Migration/DKMigrationError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Foundation

public enum DKMigrationError: Error {
case versionNotFound(filename: String)
case migrationPathNotFound
case modelNotFound(name: String?, file: String?)
}
43 changes: 43 additions & 0 deletions DataKernel/Classes/Migration/DKMigrationFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Foundation
import CoreData

public struct DKMigrationFactory {
public let models: DKModels
public let versionPolicy: DKVersionPolicy

public func migrations(from: NSManagedObjectModel, to: NSManagedObjectModel) throws -> [DKMigration] {
let modelFiles = try sortedModelFilesByVersionDesc()
var migrations = [DKMigration]()
var targetModel: NSManagedObjectModel? = nil
for modelFile in modelFiles {
guard let sourceModel = NSManagedObjectModel(contentsOf: URL(fileURLWithPath: modelFile)) else {
throw DKMigrationError.modelNotFound(name: nil, file: modelFile)
}
if let target = targetModel {
let mapping = NSMappingModel(from: [models.bundle], forSourceModel: sourceModel, destinationModel: target)
let migration = DKMigration(from: sourceModel, to: target, mapping: mapping)
migrations.append(migration)
if sourceModel == from {
return migrations.reversed()
}
targetModel = sourceModel
} else {
if sourceModel == to {
targetModel = sourceModel
}
}
}
throw DKMigrationError.migrationPathNotFound
}

private func sortedModelFilesByVersionDesc() throws -> [String] {
let fileAndVersion = try models.files.compactMap { (file: String) -> (file: String, version: Int)? in
let modelFile = URL(fileURLWithPath: file)
let version = try self.versionPolicy.version(modelUrl: modelFile)
return (file: file, version: version)
}
return fileAndVersion
.sorted(by: { $0.version > $1.version })
.map { $0.file }
}
}
49 changes: 49 additions & 0 deletions DataKernel/Classes/Migration/DKModels.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Foundation
import CoreData

public struct DKModels {
public let name: String
public let bundle: Bundle

public init(name: String, bundle: Bundle? = nil) {
self.name = name
self.bundle = bundle ?? Bundle.main
}

public var files: [String] {
if let modelDir = self.bundle.path(forResource: self.name, ofType: "momd") {
let modelDirName = NSURL(fileURLWithPath: modelDir, isDirectory: true).lastPathComponent
return self.bundle.paths(forResourcesOfType: "mom", inDirectory: modelDirName)
}
return []
}

public func modelFile(name: String) throws -> String {
guard let file = files.first(where: {
name == NSURL(fileURLWithPath: $0, isDirectory: false).deletingPathExtension?.lastPathComponent
}) else {
throw DKMigrationError.modelNotFound(name: name, file: nil)
}
return file
}

public func model(name: String) throws -> NSManagedObjectModel {
let file = try modelFile(name: name)
guard let managedModel = NSManagedObjectModel(contentsOf: URL(fileURLWithPath: file))
else {
throw DKMigrationError.modelNotFound(name: name, file: file)
}
return managedModel
}

public func currentModel() throws -> NSManagedObjectModel {
guard let modelPath = self.bundle.path(forResource: self.name, ofType: "momd") else {
throw DKMigrationError.modelNotFound(name: self.name, file: nil)
}
if let model = NSManagedObjectModel(contentsOf: URL(fileURLWithPath: modelPath)) {
return model
} else {
throw DKMigrationError.modelNotFound(name: self.name, file: modelPath)
}
}
}
42 changes: 42 additions & 0 deletions DataKernel/Classes/Migration/DKProgressiveStoreLoader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation
import CoreData

public class DKProgressiveStoreLoader: DKStoreLoader {
public let models: DKModels
public let migrationFactory: DKMigrationFactory

public init(models: DKModels, versionPolicy: DKVersionPolicy? = nil) {
self.models = models
let versionPolicy = versionPolicy ?? DKVersionFromFileNamePolicy(prefix: models.name)
self.migrationFactory = DKMigrationFactory(models: models, versionPolicy: versionPolicy)
}

public func append(store: URL, ofType: String, to coordinator: NSPersistentStoreCoordinator) throws -> NSPersistentStore {
let fileExists = FileManager.default.fileExists(atPath: store.path)
if fileExists {
try migrateIfNeeded(store: store, ofType: ofType, targetModel: coordinator.managedObjectModel)
}
let options: [AnyHashable: Any] = [
NSSQLitePragmasOption: [
"journal_mode": "WAL"
]
]
return try coordinator.addPersistentStore(
ofType: ofType,
configurationName: nil,
at: store,
options: options)
}

private func migrateIfNeeded(store: URL, ofType type: String, targetModel: NSManagedObjectModel) throws {
let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(ofType: type, at: store, options: nil)
if targetModel.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata) {
return
}
let storeModel = NSManagedObjectModel.mergedModel(from: [models.bundle], forStoreMetadata: metadata)!
let migrations = try migrationFactory.migrations(from: storeModel, to: targetModel)
for migration in migrations {
try migration.apply(url: store, sourceStoreType: type, targetStoreType: type)
}
}
}
31 changes: 31 additions & 0 deletions DataKernel/Classes/Migration/DKStoreFile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Foundation

public struct DKStoreFile {
public let url: URL

public func remove() throws {
let fileManager = FileManager.default
for path in paths() {
if fileManager.fileExists(atPath: path) {
try fileManager.removeItem(atPath: path)
}
}
}

public func move(to: URL) throws {
try DKStoreFile(url: to).remove()
let fileManager = FileManager.default
let sourcePaths = paths()
let targetPaths = paths(from: to)
for i in 0..<sourcePaths.count {
if fileManager.fileExists(atPath: sourcePaths[i]) {
try fileManager.moveItem(atPath: sourcePaths[i], toPath: targetPaths[i])
}
}
}

private func paths(from url: URL? = nil) -> [String] {
let path = (url ?? self.url).path
return [path, path + "-shm", path + "-wal"]
}
}
17 changes: 17 additions & 0 deletions DataKernel/Classes/Migration/DKVersionPolicy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation

public protocol DKVersionPolicy {
func version(modelUrl: URL) throws -> Int
}

public struct DKVersionFromFileNamePolicy: DKVersionPolicy {
public let prefix: String
public func version(modelUrl: URL) throws -> Int {
let filename = modelUrl.deletingPathExtension().lastPathComponent
if filename.hasPrefix(prefix) {
let suffix = filename.substring(from: filename.index(filename.startIndex, offsetBy: prefix.count))
return Int(suffix.trimmingCharacters(in: CharacterSet.whitespaces)) ?? 1
}
throw DKMigrationError.versionNotFound(filename: filename)
}
}
Loading