@@ -130,6 +130,9 @@ where S == S.EntityBindingType.EntityType {
130130 precondition ( !targetId. needsIdGeneration, " Can form Backlinks for persisted entities only. " )
131131 self . resolverAndCollection = ResolverAndCollection ( {
132132 do {
133+ #if DEBUG
134+ print ( " RESOLVE " , " propertyId: " , sourceProperty. propertyId, " entityId: " , targetId. value)
135+ #endif
133136 return try sourceBox
134137 . backlinkIds ( propertyId: sourceProperty. propertyId, entityId: targetId. value)
135138 . compactMap { try sourceBox. get ( id: $0. value) }
@@ -148,6 +151,9 @@ where S == S.EntityBindingType.EntityType {
148151 // Entity hasn't been written yet and has no array? It's empty.
149152 guard sourceId. value != 0 else { return [ ] }
150153 do {
154+ #if DEBUG
155+ print ( " RESOLVE STANDALONE " , " relationId: " , relationId, " sourceId: " , sourceId. value)
156+ #endif
151157 let ids : [ ReferencedType . EntityBindingType . IdType ] = try targetBox. relationTargetIds (
152158 relationId: relationId,
153159 sourceId: sourceId. value,
@@ -173,6 +179,9 @@ where S == S.EntityBindingType.EntityType {
173179 // Entity hasn't been written yet and has no array? It's empty.
174180 guard targetId. value != 0 else { return [ ] }
175181 do {
182+ #if DEBUG
183+ print ( " RESOLVE STANDALONE_BACK " , " relationId: " , relationId, " targetId: " , targetId. value)
184+ #endif
176185 return try sourceBox
177186 . relationSourceIds ( relationId: relationId, targetId: targetId. value, targetType: OwningType . self)
178187 . compactMap { try sourceBox. get ( $0. value) }
@@ -203,35 +212,42 @@ where S == S.EntityBindingType.EntityType {
203212 }
204213
205214 /// - Important: Must lock relationCacheLock to call this.
206- internal func applyToManyToDb ( relationId: obx_schema_id , referencedId : Id ) throws {
215+ internal func applyToManyStandaloneToDb ( relationId: obx_schema_id , ownerObjectId : Id ) throws {
207216 guard let owningBox = owningBox else { return }
208- if referencedId == 0 {
217+ if ownerObjectId == 0 {
209218 throw ObjectBoxError . cannotRelateToUnsavedEntities ( message: " Referenced object hasn't been put yet. " )
210219 }
211- for currEntity in removed {
212- if currEntity. entityId == 0 {
220+ // Note: decided against sorted IDs; relations are written two way ({SOURCE}{TARGET} and {TARGET}{SOURCE}).
221+ // Thus the internal relation cursor has to seek back and forth anyway, probably voiding any perf gain.
222+
223+ for target in removed {
224+ if target. entityId == 0 {
213225 throw ObjectBoxError . cannotRelateToUnsavedEntities ( message: " Owning object hasn't been put yet. " )
214226 }
215- try check ( error: obx_box_rel_remove ( owningBox, relationId, currEntity. entityId, referencedId) )
227+ let obxErr = obx_box_rel_remove ( owningBox, relationId, ownerObjectId, target. entityId)
228+ try check ( error: obxErr, message: " Could not remove relation data " )
216229 }
217- for currEntity in added {
218- if currEntity . entityId == 0 {
230+ for target in added {
231+ if target . entityId == 0 {
219232 throw ObjectBoxError . cannotRelateToUnsavedEntities ( message: " Owning object hasn't been put yet. " )
220233 }
221- try check ( error: obx_box_rel_put ( owningBox, relationId, referencedId, currEntity. entityId) )
234+ let obxErr = obx_box_rel_put ( owningBox, relationId, ownerObjectId, target. entityId)
235+ try check ( error: obxErr, message: " Could not add relation data " )
222236 }
223237 }
224238
225239 /// - Important: Must lock relationCacheLock to call this.
226- internal func applyToManyBacklinkToDb( relationId: obx_schema_id , owningId: Id ) throws {
240+ internal func applyToManyStandaloneBacklinkToDb( relationId: obx_schema_id , ownerObjectId: Id ) throws {
241+ // Need to use the target box as it owns the relation.
242+ // Thus we need to "reverse" the direction, e.g. the owning object of the ToMany becomes the relation target.
227243 guard let referencedBox = referencedBox else { return }
228- for currEntity in removed {
244+ for target in removed {
229245 try referencedBox. removeRelation ( relationId: relationId,
230- owningId : owningId , referencedId : currEntity . entityId )
246+ sourceId : target . entityId , targetId : ownerObjectId )
231247 }
232- for currEntity in added {
248+ for target in added {
233249 try referencedBox. putRelation ( relationId: relationId,
234- owningId : owningId , referencedId : currEntity . entityId )
250+ sourceId : target . entityId , targetId : ownerObjectId )
235251 }
236252 }
237253
@@ -261,13 +277,13 @@ where S == S.EntityBindingType.EntityType {
261277 throw ObjectBoxError . cannotRelateToUnsavedEntities ( message: " Owning entity of backlink hasn't "
262278 + " been put yet. " )
263279 }
264- try applyToManyBacklinkToDb ( relationId: relationId, owningId : owningId)
280+ try applyToManyStandaloneBacklinkToDb ( relationId: relationId, ownerObjectId : owningId)
265281 } else if case . toMany( let relationId, let referencedId) = info {
266282 if referencedId == 0 {
267283 throw ObjectBoxError . cannotRelateToUnsavedEntities ( message: " Related entity hasn't "
268284 + " been put yet " )
269285 }
270- try applyToManyToDb ( relationId: relationId, referencedId : referencedId)
286+ try applyToManyStandaloneToDb ( relationId: relationId, ownerObjectId : referencedId)
271287 }
272288 }
273289 } else {
@@ -338,24 +354,52 @@ extension ToMany: RangeReplaceableCollection {
338354 public convenience init ( ) {
339355 self . init ( nilLiteral: ( ) )
340356 }
341-
357+
342358 public func replaceSubrange< C, R> ( _ subrange: R , with newElements: __owned C)
343- where C: Collection , R: RangeExpression , ReferencedType == C . Element , Index == R . Bound {
344- relationCacheLock. wait ( )
345- defer { relationCacheLock. signal ( ) }
346- if resolverAndCollection. collection. isEmpty && newElements. isEmpty { return }
347-
348- let replacedComparableElements = resolverAndCollection. collection [ subrange] . map {
349- return IdComparableReferencedType ( entity: $0)
359+ where C: Collection , R: RangeExpression , ReferencedType == C . Element , Index == R . Bound
360+ {
361+ relationCacheLock. wait ( )
362+ defer { relationCacheLock. signal ( ) }
363+ if resolverAndCollection. collection. isEmpty && newElements. isEmpty { return }
364+
365+ let slice : ArraySlice < S > = resolverAndCollection. collection [ subrange]
366+ var removeSet = Set < IdComparableReferencedType > ( minimumCapacity: slice. count)
367+ for obj in slice {
368+ removeSet. insert ( IdComparableReferencedType ( entity: obj) )
369+ }
370+
371+ var addSet = Set < IdComparableReferencedType > ( minimumCapacity: newElements. count)
372+ for obj in newElements {
373+ let wrapped = IdComparableReferencedType ( entity: obj)
374+ if wrapped. entityId != 0 {
375+ if removeSet. contains ( wrapped) {
376+ removeSet. remove ( wrapped) // unchanged relation; neither add nor remove
377+ } else {
378+ addSet. insert ( wrapped) // actually new
379+ }
380+ } else {
381+ // We cannot throw here, better this than nothing for now:
382+ print ( " Warning: ignoring new object (its ID is 0) in ToMany (unsupported as of now) " )
383+ }
384+ }
385+
386+ for objRemove in removeSet {
387+ if added. contains ( objRemove) {
388+ added. remove ( objRemove) // remove after add: cancel add
389+ } else {
390+ removed. insert ( objRemove) // actually remove
350391 }
351- let newComparableElements = newElements. map { return IdComparableReferencedType ( entity: $0) }
352-
353- newComparableElements. forEach { removed. remove ( $0) }
354- replacedComparableElements. forEach { removed. insert ( $0) }
355- replacedComparableElements. forEach { added. remove ( $0) }
356- newComparableElements. forEach { added. insert ( $0) }
357-
358- resolverAndCollection. collection. replaceSubrange ( subrange, with: newElements)
392+ }
393+
394+ for objAdd in addSet {
395+ if removed. contains ( objAdd) {
396+ removed. remove ( objAdd) // add after remove: cancel remove
397+ } else {
398+ added. insert ( objAdd) // actually add
399+ }
400+ }
401+
402+ resolverAndCollection. collection. replaceSubrange ( subrange, with: newElements)
359403 }
360404}
361405
@@ -383,7 +427,11 @@ extension ToMany: CustomDebugStringConvertible {
383427extension ToMany {
384428 /// Helper object to provide custom comparison to entities based on ID,
385429 /// so we can keep a Set of entities.
386- struct IdComparableReferencedType : Hashable {
430+ struct IdComparableReferencedType : Hashable , Comparable {
431+ static func < ( lhs: ToMany < S > . IdComparableReferencedType , rhs: ToMany < S > . IdComparableReferencedType ) -> Bool {
432+ return lhs. entityId < rhs. entityId
433+ }
434+
387435 let entity : ReferencedType
388436
389437 var entityId : Id {
0 commit comments