diff --git a/CoreStoreTests/ImportTests.swift b/CoreStoreTests/ImportTests.swift index 3435b97..5b173e9 100644 --- a/CoreStoreTests/ImportTests.swift +++ b/CoreStoreTests/ImportTests.swift @@ -414,6 +414,65 @@ class ImportTests: BaseTestDataTestCase { } } + @objc + dynamic func test_ThatImportUniqueObjects_ImportsLastOfImportSourcesWithSameIDs() { + + self.prepareStack { (stack) in + + self.prepareTestDataForStack(stack) + + stack.beginSynchronous { (transaction) in + + do { + + let sourceArray: [TestEntity1.ImportSource] = [ + [ + #keyPath(TestEntity1.testEntityID): NSNumber(value: 106), + #keyPath(TestEntity1.testBoolean): NSNumber(value: true), + #keyPath(TestEntity1.testNumber): NSNumber(value: 6), + #keyPath(TestEntity1.testDecimal): NSDecimalNumber(string: "6"), + #keyPath(TestEntity1.testString): "nil:TestEntity1:6", + #keyPath(TestEntity1.testData): ("nil:TestEntity1:6" as NSString).data(using: String.Encoding.utf8.rawValue)!, + #keyPath(TestEntity1.testDate): self.dateFormatter.date(from: "2000-01-06T00:00:00Z")! + ], + [ + #keyPath(TestEntity1.testEntityID): NSNumber(value: 106), + #keyPath(TestEntity1.testBoolean): NSNumber(value: false), + #keyPath(TestEntity1.testNumber): NSNumber(value: 7), + #keyPath(TestEntity1.testDecimal): NSDecimalNumber(string: "7"), + #keyPath(TestEntity1.testString): "nil:TestEntity1:7", + #keyPath(TestEntity1.testData): ("nil:TestEntity1:7" as NSString).data(using: String.Encoding.utf8.rawValue)!, + #keyPath(TestEntity1.testDate): self.dateFormatter.date(from: "2000-01-07T00:00:00Z")! + ] + ] + let objects = try transaction.importUniqueObjects( + Into(), + sourceArray: sourceArray + ) + + XCTAssertEqual(objects.count, 1) + XCTAssertEqual(transaction.fetchCount(From()), 6) + + let object = objects[0] + let dictionary = sourceArray[1] + XCTAssertEqual(object.testEntityID, dictionary[(#keyPath(TestEntity1.testEntityID))] as? NSNumber) + XCTAssertEqual(object.testBoolean, dictionary[(#keyPath(TestEntity1.testBoolean))] as? NSNumber) + XCTAssertEqual(object.testNumber, dictionary[(#keyPath(TestEntity1.testNumber))] as? NSNumber) + XCTAssertEqual(object.testDecimal, dictionary[(#keyPath(TestEntity1.testDecimal))] as? NSDecimalNumber) + XCTAssertEqual(object.testString, dictionary[(#keyPath(TestEntity1.testString))] as? String) + XCTAssertEqual(object.testData, dictionary[(#keyPath(TestEntity1.testData))] as? Data) + XCTAssertEqual(object.testDate, dictionary[(#keyPath(TestEntity1.testDate))] as? Date) + } + catch { + + XCTFail() + } + transaction.context.reset() + } + } + } + + @objc dynamic func test_ThatImportUniqueObject_CanThrowError() { diff --git a/Sources/Importing/BaseDataTransaction+Importing.swift b/Sources/Importing/BaseDataTransaction+Importing.swift index fdc9aed..0d32c37 100644 --- a/Sources/Importing/BaseDataTransaction+Importing.swift +++ b/Sources/Importing/BaseDataTransaction+Importing.swift @@ -184,7 +184,9 @@ public extension BaseDataTransaction { /** Updates existing `ImportableUniqueObject`s or creates them by importing from the specified array of import sources. - - Warning: While the array returned from `importUniqueObjects(...)` correctly maps to the order of `sourceArray`, the order of objects called with `ImportableUniqueObject` methods is arbitrary. Do not make assumptions that any particular object will be imported ahead or after another object. + Objects are called with `ImportableUniqueObject` methods in the same order as in import sources array. + The array returned from `importUniqueObjects(...)` correctly maps to the order of `sourceArray`. + If `sourceArray` contains multiple import sources with same ID, the last one will be imported. - parameter into: an `Into` clause specifying the entity type - parameter sourceArray: the array of objects to import values from @@ -206,7 +208,7 @@ public extension BaseDataTransaction { let entityType = into.entityClass as! T.Type - var mapping = Dictionary() + var importSourceByID = Dictionary() let sortedIDs = try autoreleasepool { return try sourceArray.flatMap { (source) -> T.UniqueIDType? in @@ -215,50 +217,47 @@ public extension BaseDataTransaction { return nil } - - mapping[uniqueIDValue] = source + // each subsequent import source with the same ID will replace the existing one + importSourceByID[uniqueIDValue] = source return uniqueIDValue } } - mapping = try autoreleasepool { try preProcess(mapping) } - - var objects = Dictionary() - for object in self.fetchAll(From(entityType), Where(entityType.uniqueIDKeyPath, isMemberOf: sortedIDs)) ?? [] { + importSourceByID = try autoreleasepool { try preProcess(importSourceByID) } + + var existingObjectsByID = Dictionary() + self.fetchAll(From(entityType), Where(entityType.uniqueIDKeyPath, isMemberOf: sortedIDs))? + .forEach { existingObjectsByID[$0.uniqueIDValue] = $0 } + + var processedObjectIDs = Set() + var result = [T]() + + for objectID in sortedIDs { try autoreleasepool { - - let uniqueIDValue = object.uniqueIDValue - - guard let source = mapping.removeValue(forKey: uniqueIDValue), - entityType.shouldUpdate(from: source, in: self) else { - - return + + guard let source = importSourceByID[objectID], !processedObjectIDs.contains(objectID) else { return } + + if let object = existingObjectsByID[objectID] { + guard entityType.shouldUpdate(from: source, in: self) else { return } + + try object.update(from: source, in: self) + + result.append(object) } - - try object.update(from: source, in: self) - objects[uniqueIDValue] = object + else if entityType.shouldInsert(from: source, in: self) { + let object = self.create(into) + object.uniqueIDValue = objectID + try object.didInsert(from: source, in: self) + + result.append(object) + } + + processedObjectIDs.insert(objectID) } } - - for (uniqueIDValue, source) in mapping { - - try autoreleasepool { - - guard entityType.shouldInsert(from: source, in: self) else { - - return - } - - let object = self.create(into) - object.uniqueIDValue = uniqueIDValue - try object.didInsert(from: source, in: self) - - objects[uniqueIDValue] = object - } - } - - return sortedIDs.flatMap { objects[$0] } + + return result } } }