Skip to content

Commit

Permalink
Improve custom schema mapping provider
Browse files Browse the repository at this point in the history
1. Improve property and relationship mapping.
2. Throw errors when property or relationship cannot be mapped, don't explicitly unwrap.
3. Add extensive schema mapping and migration tests.
  • Loading branch information
iby committed Mar 16, 2019
1 parent 55c6ec4 commit f94dc88
Show file tree
Hide file tree
Showing 3 changed files with 234 additions and 86 deletions.
209 changes: 185 additions & 24 deletions CoreStoreTests/MigrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,46 +29,207 @@ import XCTest
import CoreStore


// MARK: - MigrationTests

final class MigrationTests: BaseTestCase {
func test_ThatCustomSchemaMappingProvider_CanInferTransformation() {
struct V1 {
class Animal: CoreStoreObject {
var name = Value.Required<String>("name", initial: "")
}

func test_ThatEntityDescriptionExtension_CanMapAttributes() {

// Should match attributes by renaming identifier.
do {
let src = NSEntityDescription([NSAttributeDescription("foo")])
let dst = NSEntityDescription([NSAttributeDescription("bar", renamingIdentifier: "foo")])

var map: [NSAttributeDescription: NSAttributeDescription] = [:]
XCTAssertNoThrow(map = try dst.mapAttributes(in: src))
XCTAssertEqual(map.count, 1)
XCTAssertEqual(map.keys.first?.renamingIdentifier, "foo")
XCTAssertEqual(map.values.first?.name, "foo")
}

// Should match attributes by name when matching by renaming identifier fails.
do {
let src = NSEntityDescription([NSAttributeDescription("bar")])
let dst = NSEntityDescription([NSAttributeDescription("bar", renamingIdentifier: "foo")])

var map: [NSAttributeDescription: NSAttributeDescription] = [:]
XCTAssertNoThrow(map = try dst.mapAttributes(in: src))
XCTAssertEqual(map.count, 1)
XCTAssertEqual(map.keys.first?.renamingIdentifier, "foo")
XCTAssertEqual(map.keys.first?.name, "bar")
XCTAssertEqual(map.values.first?.name, "bar")
}

// Should not throw exception when optional attributes cannot be matched.
do {
let src = NSEntityDescription([NSAttributeDescription("foo")])
let dst = NSEntityDescription([NSAttributeDescription("bar")])

var map: [NSAttributeDescription: NSAttributeDescription] = [:]
XCTAssertNoThrow(map = try dst.mapAttributes(in: src))
XCTAssertEqual(map.count, 0)
}

// Should not throw exception when required attributes with default value cannot be matched.
do {
let src = NSEntityDescription([NSAttributeDescription("foo")])
let dst = NSEntityDescription([NSAttributeDescription("bar", optional: false, defaultValue: "baz")])

var map: [NSAttributeDescription: NSAttributeDescription] = [:]
XCTAssertNoThrow(map = try dst.mapAttributes(in: src))
XCTAssertEqual(map.count, 0)
}

// Should throw exception when required attributes without default value cannot be matched.
do {
let src = NSEntityDescription([NSAttributeDescription("foo")])
let dst = NSEntityDescription([NSAttributeDescription("bar", optional: false)])
XCTAssertThrowsError(try dst.mapAttributes(in: src))
}
}

func test_ThatCustomSchemaMappingProvider_CanDeleteAndInsertEntitiesWithCustomEntityMapping() {
class Foo: CoreStoreObject {
var name = Value.Optional<String>("name")
}

class Bar: CoreStoreObject {
var nickname = Value.Optional<String>("nickname", renamingIdentifier: "name")
}

struct V2 {
class Animal: CoreStoreObject {
var nickname = Value.Required<String>("nickname", initial: "", renamingIdentifier: "name")
}
let src: CoreStoreSchema = CoreStoreSchema(modelVersion: "1", entities: [Entity<Foo>("Foo")])
let dst: CoreStoreSchema = CoreStoreSchema(modelVersion: "2", entities: [Entity<Bar>("Bar")])

let migration: CustomSchemaMappingProvider = CustomSchemaMappingProvider(from: "1", to: "2", entityMappings: [
.deleteEntity(sourceEntity: "Foo"),
.insertEntity(destinationEntity: "Bar")
])

/// Create the source store and data set.
withExtendedLifetime(DataStack(src), { stack in
try! stack.addStorageAndWait(SQLiteStore())
try! stack.perform(synchronous: { $0.create(Into<Foo>()).name.value = "Willy" })
})

let expectation: XCTestExpectation = self.expectation(description: "migration-did-complete")

withExtendedLifetime(DataStack(src, dst, migrationChain: ["1", "2"]), { stack in
_ = stack.addStorage(SQLiteStore(fileURL: SQLiteStore.defaultFileURL, migrationMappingProviders: [migration]), completion: {
switch $0 {
case .success(_):
XCTAssertEqual(stack.modelSchema.rawModel().entities.count, 1)
try! stack.perform(synchronous: { $0.create(Into<Bar>()).nickname.value = "Bobby" })
case .failure(let error):
XCTFail("\(error)")
}
expectation.fulfill()
})
})

self.waitAndCheckExpectations()
}

func test_ThatCustomSchemaMappingProvider_CanCopyEntityWithCustomEntityMapping() {
class Foo: CoreStoreObject {
var name = Value.Required<String>("name", initial: "")
}

let schemaV1: CoreStoreSchema = CoreStoreSchema(modelVersion: "V1", entities: [Entity<V1.Animal>("Animal")])
let schemaV2: CoreStoreSchema = CoreStoreSchema(modelVersion: "V2", entities: [Entity<V2.Animal>("Animal")])
let migration: CustomSchemaMappingProvider = CustomSchemaMappingProvider(from: "V1", to: "V2", entityMappings: [])
// Todo: The way this handles different version locks is flaky… It fails face on the ground in debug, but seems
// todo: to work fine in production, yet it's not clear if it transforms everything as expected.

let src: CoreStoreSchema = CoreStoreSchema(modelVersion: "1", entities: [Entity<Foo>("Foo")])
let dst: CoreStoreSchema = CoreStoreSchema(modelVersion: "2", entities: [Entity<Foo>("Foo")])

XCTAssertEqual(dst.rawModel().entities.first!.versionHash, src.rawModel().entities.first!.versionHash)

let migration: CustomSchemaMappingProvider = CustomSchemaMappingProvider(from: "1", to: "2", entityMappings: [
.copyEntity(sourceEntity: "Foo", destinationEntity: "Foo")
])

/// Create the source store and data set.
withExtendedLifetime(DataStack(schemaV1), { stack in
withExtendedLifetime(DataStack(src), { stack in
try! stack.addStorageAndWait(SQLiteStore())
try! stack.perform(synchronous: { $0.create(Into<V1.Animal>()).name.value = "Willy" })
try! stack.perform(synchronous: { XCTAssertEqual(try! $0.fetchOne(From<V1.Animal>())?.name.value, "Willy") })
try! stack.perform(synchronous: { $0.create(Into<Foo>()).name.value = "Willy" })
})

let stack: DataStack = DataStack(schemaV1, schemaV2, migrationChain: ["V1", "V2"])
let store: SQLiteStore = SQLiteStore(fileURL: SQLiteStore.defaultFileURL, migrationMappingProviders: [migration])
let expectation: XCTestExpectation = self.expectation(description: "migration-did-complete")

withExtendedLifetime(DataStack(src, dst, migrationChain: ["1", "2"]), { stack in
_ = stack.addStorage(SQLiteStore(fileURL: SQLiteStore.defaultFileURL, migrationMappingProviders: [migration]), completion: {
switch $0 {
case .success(_):
XCTAssertEqual(stack.modelSchema.rawModel().entities.count, 1)
XCTAssertEqual(try! stack.fetchCount(From<Foo>()), 1)
try! stack.perform(synchronous: { $0.create(Into<Foo>()).name.value = "Bobby" })
case .failure(let error):
XCTFail("\(error)")
}
expectation.fulfill()
})
})

self.waitAndCheckExpectations()
}

func test_ThatCustomSchemaMappingProvider_CanTransformEntityWithCustomEntityMapping() {
class Foo: CoreStoreObject {
var name = Value.Required<String>("name", initial: "")
var futile = Value.Required<String>("futile", initial: "")
}

class Bar: CoreStoreObject {
var firstName = Value.Required<String>("firstName", initial: "", renamingIdentifier: "name")
var lastName = Value.Required<String>("lastName", initial: "", renamingIdentifier: "placeholder")
var age = Value.Required<Int>("age", initial: 18)
var gender = Value.Optional<String>("gender")
}

let src: CoreStoreSchema = CoreStoreSchema(modelVersion: "1", entities: [Entity<Foo>("Foo")])
let dst: CoreStoreSchema = CoreStoreSchema(modelVersion: "2", entities: [Entity<Bar>("Bar")])

let migration: CustomSchemaMappingProvider = CustomSchemaMappingProvider(from: "1", to: "2", entityMappings: [
.transformEntity(sourceEntity: "Foo", destinationEntity: "Bar", transformer: CustomSchemaMappingProvider.CustomMapping.inferredTransformation)
])

/// Create the source store and data set.
withExtendedLifetime(DataStack(src), { stack in
try! stack.addStorageAndWait(SQLiteStore())
try! stack.perform(synchronous: { $0.create(Into<Foo>()).name.value = "Willy" })
})

let expectation: XCTestExpectation = self.expectation(description: "migration-did-complete")

_ = stack.addStorage(store, completion: {
switch $0 {
case .success(_):
XCTAssertEqual(try! stack.perform(synchronous: { try $0.fetchOne(From<V2.Animal>())?.nickname.value }), "Willy")
withExtendedLifetime(DataStack(src, dst, migrationChain: ["1", "2"]), { stack in
_ = stack.addStorage(SQLiteStore(fileURL: SQLiteStore.defaultFileURL, migrationMappingProviders: [migration]), completion: {
switch $0 {
case .success(_):
XCTAssertEqual(stack.modelSchema.rawModel().entities.count, 1)
XCTAssertEqual(try! stack.fetchCount(From<Bar>()), 1)
try! stack.perform(synchronous: { $0.create(Into<Bar>()).firstName.value = "Bobby" })
case .failure(let error):
XCTFail("\(error)")
}
expectation.fulfill()
case .failure(let error):
XCTFail("\(error)")
}
})
})

self.waitAndCheckExpectations()
}
}

extension NSEntityDescription {
fileprivate convenience init(_ properties: [NSPropertyDescription]) {
self.init()
self.properties = properties
}
}

extension NSAttributeDescription {
fileprivate convenience init(_ name: String, renamingIdentifier: String? = nil, optional: Bool? = nil, defaultValue: Any? = nil) {
self.init()
self.name = name
self.renamingIdentifier = renamingIdentifier
self.isOptional = optional ?? true
self.defaultValue = defaultValue
}
}
Loading

0 comments on commit f94dc88

Please sign in to comment.