@@ -130,6 +130,9 @@ where S == S.EntityBindingType.EntityType {
130
130
precondition ( !targetId. needsIdGeneration, " Can form Backlinks for persisted entities only. " )
131
131
self . resolverAndCollection = ResolverAndCollection ( {
132
132
do {
133
+ #if DEBUG
134
+ print ( " RESOLVE " , " propertyId: " , sourceProperty. propertyId, " entityId: " , targetId. value)
135
+ #endif
133
136
return try sourceBox
134
137
. backlinkIds ( propertyId: sourceProperty. propertyId, entityId: targetId. value)
135
138
. compactMap { try sourceBox. get ( id: $0. value) }
@@ -148,6 +151,9 @@ where S == S.EntityBindingType.EntityType {
148
151
// Entity hasn't been written yet and has no array? It's empty.
149
152
guard sourceId. value != 0 else { return [ ] }
150
153
do {
154
+ #if DEBUG
155
+ print ( " RESOLVE STANDALONE " , " relationId: " , relationId, " sourceId: " , sourceId. value)
156
+ #endif
151
157
let ids : [ ReferencedType . EntityBindingType . IdType ] = try targetBox. relationTargetIds (
152
158
relationId: relationId,
153
159
sourceId: sourceId. value,
@@ -173,6 +179,9 @@ where S == S.EntityBindingType.EntityType {
173
179
// Entity hasn't been written yet and has no array? It's empty.
174
180
guard targetId. value != 0 else { return [ ] }
175
181
do {
182
+ #if DEBUG
183
+ print ( " RESOLVE STANDALONE_BACK " , " relationId: " , relationId, " targetId: " , targetId. value)
184
+ #endif
176
185
return try sourceBox
177
186
. relationSourceIds ( relationId: relationId, targetId: targetId. value, targetType: OwningType . self)
178
187
. compactMap { try sourceBox. get ( $0. value) }
@@ -203,35 +212,42 @@ where S == S.EntityBindingType.EntityType {
203
212
}
204
213
205
214
/// - 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 {
207
216
guard let owningBox = owningBox else { return }
208
- if referencedId == 0 {
217
+ if ownerObjectId == 0 {
209
218
throw ObjectBoxError . cannotRelateToUnsavedEntities ( message: " Referenced object hasn't been put yet. " )
210
219
}
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 {
213
225
throw ObjectBoxError . cannotRelateToUnsavedEntities ( message: " Owning object hasn't been put yet. " )
214
226
}
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 " )
216
229
}
217
- for currEntity in added {
218
- if currEntity . entityId == 0 {
230
+ for target in added {
231
+ if target . entityId == 0 {
219
232
throw ObjectBoxError . cannotRelateToUnsavedEntities ( message: " Owning object hasn't been put yet. " )
220
233
}
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 " )
222
236
}
223
237
}
224
238
225
239
/// - 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.
227
243
guard let referencedBox = referencedBox else { return }
228
- for currEntity in removed {
244
+ for target in removed {
229
245
try referencedBox. removeRelation ( relationId: relationId,
230
- owningId : owningId , referencedId : currEntity . entityId )
246
+ sourceId : target . entityId , targetId : ownerObjectId )
231
247
}
232
- for currEntity in added {
248
+ for target in added {
233
249
try referencedBox. putRelation ( relationId: relationId,
234
- owningId : owningId , referencedId : currEntity . entityId )
250
+ sourceId : target . entityId , targetId : ownerObjectId )
235
251
}
236
252
}
237
253
@@ -261,13 +277,13 @@ where S == S.EntityBindingType.EntityType {
261
277
throw ObjectBoxError . cannotRelateToUnsavedEntities ( message: " Owning entity of backlink hasn't "
262
278
+ " been put yet. " )
263
279
}
264
- try applyToManyBacklinkToDb ( relationId: relationId, owningId : owningId)
280
+ try applyToManyStandaloneBacklinkToDb ( relationId: relationId, ownerObjectId : owningId)
265
281
} else if case . toMany( let relationId, let referencedId) = info {
266
282
if referencedId == 0 {
267
283
throw ObjectBoxError . cannotRelateToUnsavedEntities ( message: " Related entity hasn't "
268
284
+ " been put yet " )
269
285
}
270
- try applyToManyToDb ( relationId: relationId, referencedId : referencedId)
286
+ try applyToManyStandaloneToDb ( relationId: relationId, ownerObjectId : referencedId)
271
287
}
272
288
}
273
289
} else {
@@ -338,24 +354,52 @@ extension ToMany: RangeReplaceableCollection {
338
354
public convenience init ( ) {
339
355
self . init ( nilLiteral: ( ) )
340
356
}
341
-
357
+
342
358
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
350
391
}
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)
359
403
}
360
404
}
361
405
@@ -383,7 +427,11 @@ extension ToMany: CustomDebugStringConvertible {
383
427
extension ToMany {
384
428
/// Helper object to provide custom comparison to entities based on ID,
385
429
/// 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
+
387
435
let entity : ReferencedType
388
436
389
437
var entityId : Id {
0 commit comments