Skip to content

Commit eafba21

Browse files
committed
feat: index for efficient history queries
1 parent 66fbf72 commit eafba21

File tree

3 files changed

+58
-25
lines changed

3 files changed

+58
-25
lines changed

model-datastructure/src/commonMain/kotlin/org/modelix/datastructures/model/HistoryIndexNode.kt

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import org.modelix.model.persistent.Separators
1919
import org.modelix.streams.IStream
2020
import org.modelix.streams.flatten
2121
import org.modelix.streams.plus
22+
import kotlin.math.abs
2223
import kotlin.math.max
2324
import kotlin.math.min
2425
import kotlin.time.Duration
@@ -53,6 +54,7 @@ sealed class HistoryIndexNode : IObjectData {
5354
require(self.data === this)
5455
val other = otherObj.data
5556
require(maxTime < other.minTime)
57+
require(abs(height - other.height) <= 2)
5658
return HistoryIndexRangeNode(
5759
firstVersion = firstVersion,
5860
lastVersion = other.lastVersion,
@@ -108,7 +110,7 @@ sealed class HistoryIndexNode : IObjectData {
108110
}
109111
}
110112

111-
fun of(version: Object<CPVersion>): HistoryIndexLeafNode {
113+
fun of(version: Object<CPVersion>): HistoryIndexNode {
112114
val time = CLVersion(version).getTimestamp() ?: Instant.Companion.fromEpochMilliseconds(0L)
113115
return HistoryIndexLeafNode(
114116
versions = listOf(version.ref),
@@ -124,7 +126,7 @@ sealed class HistoryIndexNode : IObjectData {
124126
}
125127

126128
fun Object<HistoryIndexNode>.merge(otherObj: Object<HistoryIndexNode>): Object<HistoryIndexNode> = data.merge(this, otherObj)
127-
fun Object<HistoryIndexNode>.concat(otherObj: Object<HistoryIndexNode>): Object<HistoryIndexNode> = data.concat(this, otherObj)
129+
fun Object<HistoryIndexNode>.concatUnbalanced(otherObj: Object<HistoryIndexNode>): Object<HistoryIndexNode> = data.concat(this, otherObj)
128130
val Object<HistoryIndexLeafNode>.time get() = data.time
129131

130132
data class HistoryIndexLeafNode(
@@ -194,8 +196,8 @@ data class HistoryIndexLeafNode(
194196
return when (other) {
195197
is HistoryIndexLeafNode -> {
196198
when {
197-
other.time < time -> otherObj.concat(self)
198-
other.time > time -> self.concat(otherObj)
199+
other.time < time -> otherObj.concatBalanced(self)
200+
other.time > time -> self.concatBalanced(otherObj)
199201
else -> HistoryIndexLeafNode(
200202
versions = (versions.associateBy { it.getHash() } + other.versions.associateBy { it.getHash() }).values.toList(),
201203
authors = authors + other.authors,
@@ -283,15 +285,15 @@ data class HistoryIndexRangeNode(
283285
val range1 = resolvedChild1.data.timeRange
284286
val range2 = resolvedChild2.data.timeRange
285287
return when {
286-
other.time < range1.start -> otherObj.concat(resolvedChild1).concat(resolvedChild2)
287-
other.time <= range1.endInclusive -> resolvedChild1.merge(otherObj).concat(resolvedChild2)
288+
other.time < range1.start -> otherObj.concatBalanced(resolvedChild1).concatBalanced(resolvedChild2)
289+
other.time <= range1.endInclusive -> resolvedChild1.merge(otherObj).concatBalanced(resolvedChild2)
288290
other.time < range2.start -> if (resolvedChild1.size <= resolvedChild2.size) {
289-
resolvedChild1.concat(otherObj).concat(resolvedChild2)
291+
resolvedChild1.concatBalanced(otherObj).concatBalanced(resolvedChild2)
290292
} else {
291-
resolvedChild1.concat(otherObj.concat(resolvedChild2))
293+
resolvedChild1.concatBalanced(otherObj.concatBalanced(resolvedChild2))
292294
}
293-
other.time <= range2.endInclusive -> resolvedChild1.concat(resolvedChild2.merge(otherObj))
294-
else -> resolvedChild1.concat(resolvedChild2.concat(otherObj))
295+
other.time <= range2.endInclusive -> resolvedChild1.concatBalanced(resolvedChild2.merge(otherObj))
296+
else -> resolvedChild1.concatBalanced(resolvedChild2.concatBalanced(otherObj))
295297
}
296298
}
297299
is HistoryIndexRangeNode -> {
@@ -303,27 +305,27 @@ data class HistoryIndexRangeNode(
303305
intersects1 && intersects2 -> {
304306
resolvedChild1.merge(otherObj).merge(resolvedChild2)
305307
}
306-
intersects1 -> resolvedChild1.merge(otherObj).concat(resolvedChild2)
307-
intersects2 -> resolvedChild1.concat(resolvedChild2.merge(otherObj))
308+
intersects1 -> resolvedChild1.merge(otherObj).concatBalanced(resolvedChild2)
309+
intersects2 -> resolvedChild1.concatBalanced(resolvedChild2.merge(otherObj))
308310
other.maxTime < range1.start -> {
309311
if (other.size < resolvedChild2.size) {
310-
otherObj.concat(resolvedChild1).concat(resolvedChild2)
312+
otherObj.concatBalanced(resolvedChild1).concatBalanced(resolvedChild2)
311313
} else {
312-
otherObj.concat(self)
314+
otherObj.concatBalanced(self)
313315
}
314316
}
315317
other.maxTime < range2.start -> {
316318
if (resolvedChild2.size < resolvedChild1.size) {
317-
resolvedChild1.concat(otherObj.concat(resolvedChild2))
319+
resolvedChild1.concatBalanced(otherObj.concatBalanced(resolvedChild2))
318320
} else {
319-
resolvedChild1.concat(otherObj).concat(resolvedChild2)
321+
resolvedChild1.concatBalanced(otherObj).concatBalanced(resolvedChild2)
320322
}
321323
}
322324
else -> {
323325
if (other.size < resolvedChild1.size) {
324-
resolvedChild1.concat(resolvedChild2.concat(otherObj))
326+
resolvedChild1.concatBalanced(resolvedChild2.concatBalanced(otherObj))
325327
} else {
326-
self.concat(otherObj)
328+
self.concatBalanced(otherObj)
327329
}
328330
}
329331
}
@@ -394,3 +396,32 @@ fun Long.rangeOfSize(size: Long) = this until (this + size)
394396
fun LongRange.intersect(other: LongRange): LongRange {
395397
return if (this.first > other.first) other.intersect(this) else other.first..min(this.last, other.last)
396398
}
399+
400+
fun Object<HistoryIndexNode>.rebalance(otherObj: Object<HistoryIndexNode>): Pair<Object<HistoryIndexNode>, Object<HistoryIndexNode>> {
401+
if (otherObj.height > height + 1) {
402+
val split1 = (otherObj.data as HistoryIndexRangeNode).child1.resolveNow()
403+
val split2 = (otherObj.data as HistoryIndexRangeNode).child2.resolveNow()
404+
val rebalanced = this.rebalance(split1)
405+
if (rebalanced.first.height <= split2.height) {
406+
return rebalanced.first.concatUnbalanced(rebalanced.second) to split2
407+
} else {
408+
return rebalanced.first to rebalanced.second.concatUnbalanced(split2)
409+
}
410+
} else if (height > otherObj.height + 1) {
411+
val split1 = (this.data as HistoryIndexRangeNode).child1.resolveNow()
412+
val split2 = (this.data as HistoryIndexRangeNode).child2.resolveNow()
413+
val rebalanced = split2.rebalance(otherObj)
414+
if (rebalanced.second.height > split1.height) {
415+
return split1.concatUnbalanced(rebalanced.first) to rebalanced.second
416+
} else {
417+
return split1 to rebalanced.first.concatUnbalanced(rebalanced.second)
418+
}
419+
} else {
420+
return this to otherObj
421+
}
422+
}
423+
424+
fun Object<HistoryIndexNode>.concatBalanced(otherObj: Object<HistoryIndexNode>): Object<HistoryIndexNode> {
425+
val rebalanced = this.rebalance(otherObj)
426+
return rebalanced.first.concatUnbalanced(rebalanced.second)
427+
}

model-datastructure/src/commonTest/kotlin/HistoryIndexTest.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class HistoryIndexTest {
5656
val version2 = newVersion(version1)
5757
val history = HistoryIndexNode.of(version1.obj, version2.obj)
5858
assertEquals(2, history.size)
59-
assertEquals(1, history.height)
59+
assertEquals(2, history.height)
6060
}
6161

6262
@Test
@@ -69,7 +69,7 @@ class HistoryIndexTest {
6969
val history2 = HistoryIndexNode.of(version3.obj).asObject(graph)
7070
val history = history1.merge(history2)
7171
assertEquals(3, history.size)
72-
assertEquals(2, history.height)
72+
assertEquals(3, history.height)
7373
}
7474

7575
@Test
@@ -83,7 +83,7 @@ class HistoryIndexTest {
8383
.merge(HistoryIndexNode.of(version3.obj).asObject(graph))
8484
.merge(HistoryIndexNode.of(version4.obj).asObject(graph))
8585
assertEquals(4, history.size)
86-
assertEquals(3, history.height)
86+
assertEquals(4, history.height)
8787
}
8888

8989
@Test
@@ -161,7 +161,7 @@ class HistoryIndexTest {
161161
versions.map { it.getObjectHash() },
162162
history.data.getAllVersions().map { it.getHash() }.toList().getBlocking(graph),
163163
)
164-
assertEquals(12, history.height)
164+
assertEquals(11, history.height)
165165
}
166166

167167
@Test
@@ -196,7 +196,7 @@ class HistoryIndexTest {
196196
versions.map { it.getObjectHash() },
197197
history.data.getAllVersions().map { it.getHash() }.toList().getBlocking(graph),
198198
)
199-
assertEquals(10, history.height)
199+
assertEquals(11, history.height)
200200
}
201201

202202
@Test

model-server/src/test/kotlin/org/modelix/model/server/HistoryIndexTest.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import org.modelix.model.server.handlers.IdsApiImpl
1212
import org.modelix.model.server.handlers.ModelReplicationServer
1313
import org.modelix.model.server.handlers.RepositoriesManager
1414
import org.modelix.model.server.store.InMemoryStoreClient
15+
import kotlin.random.Random
1516
import kotlin.test.Test
1617
import kotlin.test.assertEquals
1718
import kotlin.time.Duration.Companion.seconds
@@ -77,6 +78,7 @@ class HistoryIndexTest {
7778
@Test fun pagination_201_201() = runPaginationTest(201, 201)
7879

7980
private fun runPaginationTest(skip: Int, limit: Int) = runTest {
81+
val rand = Random(8923345)
8082
val modelClient: IModelClientV2 = createModelClient()
8183
val repositoryId = RepositoryId("repo1")
8284
val branchRef = repositoryId.getBranchReference()
@@ -89,7 +91,7 @@ class HistoryIndexTest {
8991
.baseVersion(currentVersion)
9092
.tree(currentVersion.getModelTree())
9193
.author("user1")
92-
.time(currentVersion.getTimestamp()!! + 3.seconds)
94+
.time(currentVersion.getTimestamp()!! + rand.nextInt(0, 3).seconds)
9395
.build()
9496
currentVersion = modelClient.push(branchRef, newVersion, currentVersion)
9597
}
@@ -98,7 +100,7 @@ class HistoryIndexTest {
98100
.baseVersion(currentVersion)
99101
.tree(currentVersion.getModelTree())
100102
.author("user2")
101-
.time(currentVersion.getTimestamp()!! + 2.seconds)
103+
.time(currentVersion.getTimestamp()!! + rand.nextInt(0, 3).seconds)
102104
.build()
103105
currentVersion = modelClient.push(branchRef, newVersion, currentVersion)
104106
}

0 commit comments

Comments
 (0)