Skip to content

Commit 6c7778f

Browse files
committed
Improve custom schema mapping provider
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.
1 parent 4790053 commit 6c7778f

File tree

3 files changed

+234
-86
lines changed

3 files changed

+234
-86
lines changed

CoreStoreTests/MigrationTests.swift

Lines changed: 185 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,46 +29,207 @@ import XCTest
2929
import CoreStore
3030

3131

32+
// MARK: - MigrationTests
33+
3234
final class MigrationTests: BaseTestCase {
33-
func test_ThatCustomSchemaMappingProvider_CanInferTransformation() {
34-
struct V1 {
35-
class Animal: CoreStoreObject {
36-
var name = Value.Required<String>("name", initial: "")
37-
}
35+
36+
func test_ThatEntityDescriptionExtension_CanMapAttributes() {
37+
38+
// Should match attributes by renaming identifier.
39+
do {
40+
let src = NSEntityDescription([NSAttributeDescription("foo")])
41+
let dst = NSEntityDescription([NSAttributeDescription("bar", renamingIdentifier: "foo")])
42+
43+
var map: [NSAttributeDescription: NSAttributeDescription] = [:]
44+
XCTAssertNoThrow(map = try dst.mapAttributes(in: src))
45+
XCTAssertEqual(map.count, 1)
46+
XCTAssertEqual(map.keys.first?.renamingIdentifier, "foo")
47+
XCTAssertEqual(map.values.first?.name, "foo")
48+
}
49+
50+
// Should match attributes by name when matching by renaming identifier fails.
51+
do {
52+
let src = NSEntityDescription([NSAttributeDescription("bar")])
53+
let dst = NSEntityDescription([NSAttributeDescription("bar", renamingIdentifier: "foo")])
54+
55+
var map: [NSAttributeDescription: NSAttributeDescription] = [:]
56+
XCTAssertNoThrow(map = try dst.mapAttributes(in: src))
57+
XCTAssertEqual(map.count, 1)
58+
XCTAssertEqual(map.keys.first?.renamingIdentifier, "foo")
59+
XCTAssertEqual(map.keys.first?.name, "bar")
60+
XCTAssertEqual(map.values.first?.name, "bar")
61+
}
62+
63+
// Should not throw exception when optional attributes cannot be matched.
64+
do {
65+
let src = NSEntityDescription([NSAttributeDescription("foo")])
66+
let dst = NSEntityDescription([NSAttributeDescription("bar")])
67+
68+
var map: [NSAttributeDescription: NSAttributeDescription] = [:]
69+
XCTAssertNoThrow(map = try dst.mapAttributes(in: src))
70+
XCTAssertEqual(map.count, 0)
71+
}
72+
73+
// Should not throw exception when required attributes with default value cannot be matched.
74+
do {
75+
let src = NSEntityDescription([NSAttributeDescription("foo")])
76+
let dst = NSEntityDescription([NSAttributeDescription("bar", optional: false, defaultValue: "baz")])
77+
78+
var map: [NSAttributeDescription: NSAttributeDescription] = [:]
79+
XCTAssertNoThrow(map = try dst.mapAttributes(in: src))
80+
XCTAssertEqual(map.count, 0)
81+
}
82+
83+
// Should throw exception when required attributes without default value cannot be matched.
84+
do {
85+
let src = NSEntityDescription([NSAttributeDescription("foo")])
86+
let dst = NSEntityDescription([NSAttributeDescription("bar", optional: false)])
87+
XCTAssertThrowsError(try dst.mapAttributes(in: src))
88+
}
89+
}
90+
91+
func test_ThatCustomSchemaMappingProvider_CanDeleteAndInsertEntitiesWithCustomEntityMapping() {
92+
class Foo: CoreStoreObject {
93+
var name = Value.Optional<String>("name")
94+
}
95+
96+
class Bar: CoreStoreObject {
97+
var nickname = Value.Optional<String>("nickname", renamingIdentifier: "name")
3898
}
3999

40-
struct V2 {
41-
class Animal: CoreStoreObject {
42-
var nickname = Value.Required<String>("nickname", initial: "", renamingIdentifier: "name")
43-
}
100+
let src: CoreStoreSchema = CoreStoreSchema(modelVersion: "1", entities: [Entity<Foo>("Foo")])
101+
let dst: CoreStoreSchema = CoreStoreSchema(modelVersion: "2", entities: [Entity<Bar>("Bar")])
102+
103+
let migration: CustomSchemaMappingProvider = CustomSchemaMappingProvider(from: "1", to: "2", entityMappings: [
104+
.deleteEntity(sourceEntity: "Foo"),
105+
.insertEntity(destinationEntity: "Bar")
106+
])
107+
108+
/// Create the source store and data set.
109+
withExtendedLifetime(DataStack(src), { stack in
110+
try! stack.addStorageAndWait(SQLiteStore())
111+
try! stack.perform(synchronous: { $0.create(Into<Foo>()).name.value = "Willy" })
112+
})
113+
114+
let expectation: XCTestExpectation = self.expectation(description: "migration-did-complete")
115+
116+
withExtendedLifetime(DataStack(src, dst, migrationChain: ["1", "2"]), { stack in
117+
_ = stack.addStorage(SQLiteStore(fileURL: SQLiteStore.defaultFileURL, migrationMappingProviders: [migration]), completion: {
118+
switch $0 {
119+
case .success(_):
120+
XCTAssertEqual(stack.modelSchema.rawModel().entities.count, 1)
121+
try! stack.perform(synchronous: { $0.create(Into<Bar>()).nickname.value = "Bobby" })
122+
case .failure(let error):
123+
XCTFail("\(error)")
124+
}
125+
expectation.fulfill()
126+
})
127+
})
128+
129+
self.waitAndCheckExpectations()
130+
}
131+
132+
func test_ThatCustomSchemaMappingProvider_CanCopyEntityWithCustomEntityMapping() {
133+
class Foo: CoreStoreObject {
134+
var name = Value.Required<String>("name", initial: "")
44135
}
45136

46-
let schemaV1: CoreStoreSchema = CoreStoreSchema(modelVersion: "V1", entities: [Entity<V1.Animal>("Animal")])
47-
let schemaV2: CoreStoreSchema = CoreStoreSchema(modelVersion: "V2", entities: [Entity<V2.Animal>("Animal")])
48-
let migration: CustomSchemaMappingProvider = CustomSchemaMappingProvider(from: "V1", to: "V2", entityMappings: [])
137+
// Todo: The way this handles different version locks is flaky… It fails face on the ground in debug, but seems
138+
// todo: to work fine in production, yet it's not clear if it transforms everything as expected.
139+
140+
let src: CoreStoreSchema = CoreStoreSchema(modelVersion: "1", entities: [Entity<Foo>("Foo")])
141+
let dst: CoreStoreSchema = CoreStoreSchema(modelVersion: "2", entities: [Entity<Foo>("Foo")])
142+
143+
XCTAssertEqual(dst.rawModel().entities.first!.versionHash, src.rawModel().entities.first!.versionHash)
144+
145+
let migration: CustomSchemaMappingProvider = CustomSchemaMappingProvider(from: "1", to: "2", entityMappings: [
146+
.copyEntity(sourceEntity: "Foo", destinationEntity: "Foo")
147+
])
49148

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

57-
let stack: DataStack = DataStack(schemaV1, schemaV2, migrationChain: ["V1", "V2"])
58-
let store: SQLiteStore = SQLiteStore(fileURL: SQLiteStore.defaultFileURL, migrationMappingProviders: [migration])
155+
let expectation: XCTestExpectation = self.expectation(description: "migration-did-complete")
156+
157+
withExtendedLifetime(DataStack(src, dst, migrationChain: ["1", "2"]), { stack in
158+
_ = stack.addStorage(SQLiteStore(fileURL: SQLiteStore.defaultFileURL, migrationMappingProviders: [migration]), completion: {
159+
switch $0 {
160+
case .success(_):
161+
XCTAssertEqual(stack.modelSchema.rawModel().entities.count, 1)
162+
XCTAssertEqual(try! stack.fetchCount(From<Foo>()), 1)
163+
try! stack.perform(synchronous: { $0.create(Into<Foo>()).name.value = "Bobby" })
164+
case .failure(let error):
165+
XCTFail("\(error)")
166+
}
167+
expectation.fulfill()
168+
})
169+
})
170+
171+
self.waitAndCheckExpectations()
172+
}
173+
174+
func test_ThatCustomSchemaMappingProvider_CanTransformEntityWithCustomEntityMapping() {
175+
class Foo: CoreStoreObject {
176+
var name = Value.Required<String>("name", initial: "")
177+
var futile = Value.Required<String>("futile", initial: "")
178+
}
179+
180+
class Bar: CoreStoreObject {
181+
var firstName = Value.Required<String>("firstName", initial: "", renamingIdentifier: "name")
182+
var lastName = Value.Required<String>("lastName", initial: "", renamingIdentifier: "placeholder")
183+
var age = Value.Required<Int>("age", initial: 18)
184+
var gender = Value.Optional<String>("gender")
185+
}
186+
187+
let src: CoreStoreSchema = CoreStoreSchema(modelVersion: "1", entities: [Entity<Foo>("Foo")])
188+
let dst: CoreStoreSchema = CoreStoreSchema(modelVersion: "2", entities: [Entity<Bar>("Bar")])
189+
190+
let migration: CustomSchemaMappingProvider = CustomSchemaMappingProvider(from: "1", to: "2", entityMappings: [
191+
.transformEntity(sourceEntity: "Foo", destinationEntity: "Bar", transformer: CustomSchemaMappingProvider.CustomMapping.inferredTransformation)
192+
])
193+
194+
/// Create the source store and data set.
195+
withExtendedLifetime(DataStack(src), { stack in
196+
try! stack.addStorageAndWait(SQLiteStore())
197+
try! stack.perform(synchronous: { $0.create(Into<Foo>()).name.value = "Willy" })
198+
})
59199

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

62-
_ = stack.addStorage(store, completion: {
63-
switch $0 {
64-
case .success(_):
65-
XCTAssertEqual(try! stack.perform(synchronous: { try $0.fetchOne(From<V2.Animal>())?.nickname.value }), "Willy")
202+
withExtendedLifetime(DataStack(src, dst, migrationChain: ["1", "2"]), { stack in
203+
_ = stack.addStorage(SQLiteStore(fileURL: SQLiteStore.defaultFileURL, migrationMappingProviders: [migration]), completion: {
204+
switch $0 {
205+
case .success(_):
206+
XCTAssertEqual(stack.modelSchema.rawModel().entities.count, 1)
207+
XCTAssertEqual(try! stack.fetchCount(From<Bar>()), 1)
208+
try! stack.perform(synchronous: { $0.create(Into<Bar>()).firstName.value = "Bobby" })
209+
case .failure(let error):
210+
XCTFail("\(error)")
211+
}
66212
expectation.fulfill()
67-
case .failure(let error):
68-
XCTFail("\(error)")
69-
}
213+
})
70214
})
71215

72216
self.waitAndCheckExpectations()
73217
}
74218
}
219+
220+
extension NSEntityDescription {
221+
fileprivate convenience init(_ properties: [NSPropertyDescription]) {
222+
self.init()
223+
self.properties = properties
224+
}
225+
}
226+
227+
extension NSAttributeDescription {
228+
fileprivate convenience init(_ name: String, renamingIdentifier: String? = nil, optional: Bool? = nil, defaultValue: Any? = nil) {
229+
self.init()
230+
self.name = name
231+
self.renamingIdentifier = renamingIdentifier
232+
self.isOptional = optional ?? true
233+
self.defaultValue = defaultValue
234+
}
235+
}

0 commit comments

Comments
 (0)