Skip to content

Commit 71e92f1

Browse files
committed
ToMany fixes: clarify and fix the relation direction, replaceSubrange() rework to filter out ops canceling each other, added ManyToManyTests.swift based on model from integration test (imported entity sources and related generated code)
1 parent ec46dc5 commit 71e92f1

File tree

10 files changed

+1541
-60
lines changed

10 files changed

+1541
-60
lines changed

Source/Gemfile.lock

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ GEM
77
minitest (~> 5.1)
88
thread_safe (~> 0.3, >= 0.3.4)
99
tzinfo (~> 1.1)
10-
algoliasearch (1.27.1)
10+
algoliasearch (1.27.2)
1111
httpclient (~> 2.8, >= 2.8.3)
1212
json (>= 1.5.1)
1313
atomos (0.1.3)
1414
claide (1.0.3)
15-
cocoapods (1.8.4)
15+
cocoapods (1.9.1)
1616
activesupport (>= 4.0.2, < 5)
1717
claide (>= 1.0.2, < 2.0)
18-
cocoapods-core (= 1.8.4)
18+
cocoapods-core (= 1.9.1)
1919
cocoapods-deintegrate (>= 1.0.3, < 2.0)
2020
cocoapods-downloader (>= 1.2.2, < 2.0)
2121
cocoapods-plugins (>= 1.0.0, < 2.0)
@@ -30,34 +30,38 @@ GEM
3030
molinillo (~> 0.6.6)
3131
nap (~> 1.0)
3232
ruby-macho (~> 1.4)
33-
xcodeproj (>= 1.11.1, < 2.0)
34-
cocoapods-core (1.8.4)
33+
xcodeproj (>= 1.14.0, < 2.0)
34+
cocoapods-core (1.9.1)
3535
activesupport (>= 4.0.2, < 6)
3636
algoliasearch (~> 1.0)
3737
concurrent-ruby (~> 1.1)
3838
fuzzy_match (~> 2.0.4)
3939
nap (~> 1.0)
40+
netrc (~> 0.11)
41+
typhoeus (~> 1.0)
4042
cocoapods-deintegrate (1.0.4)
4143
cocoapods-downloader (1.3.0)
4244
cocoapods-plugins (1.0.0)
4345
nap
4446
cocoapods-search (1.0.0)
4547
cocoapods-stats (1.1.0)
46-
cocoapods-trunk (1.4.1)
48+
cocoapods-trunk (1.5.0)
4749
nap (>= 0.8, < 2.0)
4850
netrc (~> 0.11)
49-
cocoapods-try (1.1.0)
51+
cocoapods-try (1.2.0)
5052
colored2 (3.1.2)
51-
concurrent-ruby (1.1.5)
53+
concurrent-ruby (1.1.6)
5254
escape (0.0.4)
53-
ffi (1.11.3)
55+
ethon (0.12.0)
56+
ffi (>= 1.3.0)
57+
ffi (1.12.2)
5458
fourflusher (2.3.1)
5559
fuzzy_match (2.0.4)
5660
gh_inspector (1.1.3)
5761
httpclient (2.8.3)
5862
i18n (0.9.5)
5963
concurrent-ruby (~> 1.0)
60-
jazzy (0.13.0)
64+
jazzy (0.13.3)
6165
cocoapods (~> 1.5)
6266
mustache (~> 1.1)
6367
open4
@@ -68,25 +72,27 @@ GEM
6872
xcinvoke (~> 0.3.0)
6973
json (2.3.0)
7074
liferaft (0.0.6)
71-
minitest (5.13.0)
75+
minitest (5.14.0)
7276
molinillo (0.6.6)
7377
mustache (1.1.1)
7478
nanaimo (0.2.6)
7579
nap (1.1.0)
7680
netrc (0.11.0)
7781
open4 (1.3.4)
7882
redcarpet (3.5.0)
79-
rouge (3.14.0)
83+
rouge (3.18.0)
8084
ruby-macho (1.4.0)
81-
sassc (2.2.1)
85+
sassc (2.3.0)
8286
ffi (~> 1.9)
83-
sqlite3 (1.4.1)
87+
sqlite3 (1.4.2)
8488
thread_safe (0.3.6)
85-
tzinfo (1.2.5)
89+
typhoeus (1.4.0)
90+
ethon (>= 0.9.0)
91+
tzinfo (1.2.7)
8692
thread_safe (~> 0.1)
8793
xcinvoke (0.3.0)
8894
liferaft (~> 0.0.6)
89-
xcodeproj (1.14.0)
95+
xcodeproj (1.16.0)
9096
CFPropertyList (>= 2.3.3, < 4.0)
9197
atomos (~> 0.1.3)
9298
claide (>= 1.0.2, < 2.0)
@@ -104,4 +110,4 @@ RUBY VERSION
104110
ruby 2.3.7p456
105111

106112
BUNDLED WITH
107-
2.0.2
113+
2.1.4

Source/ios-framework/CommonSource/Relation/Box+BacklinkIds.swift

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,23 +61,25 @@ extension Box {
6161
return idArray(sourceIds, type: SourceType.self)
6262
}
6363

64-
internal func removeRelation(relationId: obx_schema_id, owningId: Id, referencedId: Id) throws {
65-
if owningId == 0 {
64+
internal func removeRelation(relationId: obx_schema_id, sourceId: Id, targetId: Id) throws {
65+
if sourceId == 0 {
6666
throw ObjectBoxError.cannotRelateToUnsavedEntities(message: "Owning object hasn't been put yet.")
6767
}
68-
if referencedId == 0 {
68+
if targetId == 0 {
6969
throw ObjectBoxError.cannotRelateToUnsavedEntities(message: "Referenced object hasn't been put yet.")
7070
}
71-
try check(error: obx_box_rel_remove(cBox, relationId, owningId, referencedId))
71+
let obxErr = obx_box_rel_remove(cBox, relationId, sourceId, targetId)
72+
try check(error: obxErr, message: "Could not remove relation data")
7273
}
7374

74-
internal func putRelation(relationId: obx_schema_id, owningId: Id, referencedId: Id) throws {
75-
if owningId == 0 {
75+
internal func putRelation(relationId: obx_schema_id, sourceId: Id, targetId: Id) throws {
76+
if sourceId == 0 {
7677
throw ObjectBoxError.cannotRelateToUnsavedEntities(message: "Owning object hasn't been put yet.")
7778
}
78-
if referencedId == 0 {
79+
if targetId == 0 {
7980
throw ObjectBoxError.cannotRelateToUnsavedEntities(message: "Referenced object hasn't been put yet.")
8081
}
81-
try check(error: obx_box_rel_put(cBox, relationId, referencedId, owningId))
82+
let obxErr = obx_box_rel_put(cBox, relationId, sourceId, targetId)
83+
try check(error: obxErr, message: "Could not add relation data")
8284
}
8385
}

Source/ios-framework/CommonSource/Relation/ToMany.swift

Lines changed: 80 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -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 {
383427
extension 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 {

Source/ios-framework/CommonSource/Store+SwiftRefinedAPI.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ extension Store {
5151
} else {
5252
return try block(transaction)
5353
}
54-
5554
}
5655

5756
/// Internal version that gives the block a Transaction

Source/ios-framework/CommonSource/Store.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public class Store: CustomDebugStringConvertible {
4040
internal var supportsLargeArrays = false
4141

4242
/// Returns the version of ObjectBox Swift.
43-
public static var version = "1.3.0"
43+
public static var version = "1.3.1"
4444

4545
/// Returns the versions of ObjectBox Swift, the ObjectBox lib, and ObjectBox core.
4646
public static var versionAll: String {

0 commit comments

Comments
 (0)