Skip to content

Commit 54b6e93

Browse files
committed
feat(mps-sync-plugin): import MPS nodes with their original ID
1 parent f085cc3 commit 54b6e93

File tree

42 files changed

+1197
-210
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1197
-210
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.modelix.model.sync.bulk
2+
3+
import org.modelix.model.api.IMutableModel
4+
import org.modelix.model.api.INodeReference
5+
import org.modelix.model.api.IReadableNode
6+
import org.modelix.model.api.IWritableNode
7+
8+
/**
9+
* A trivial implementation that expects the source and target nodes to have the same identity,
10+
* meaning they can both be resolved by the same INodeReference.
11+
*/
12+
class IdentityPreservingNodeAssociation(
13+
val targetModel: IMutableModel,
14+
val overrides: Map<INodeReference, INodeReference>,
15+
) : INodeAssociation {
16+
17+
override fun resolveTarget(sourceNode: IReadableNode): IWritableNode? {
18+
val sourceReference = sourceNode.getNodeReference()
19+
val targetReference = overrides[sourceReference] ?: sourceReference
20+
return targetModel.resolveNode(targetReference)
21+
}
22+
23+
override fun associate(
24+
sourceNode: IReadableNode,
25+
targetNode: IWritableNode,
26+
) {
27+
val sourceReference = sourceNode.getNodeReference()
28+
val expectedTargetReference = overrides[sourceReference] ?: sourceReference
29+
val actualTargetReference = targetNode.getNodeReference()
30+
require(expectedTargetReference == actualTargetReference) {
31+
"Cannot associate $sourceReference with $actualTargetReference, expected: $expectedTargetReference"
32+
}
33+
}
34+
}

bulk-model-sync-lib/src/jvmMain/kotlin/org/modelix/model/sync/bulk/InvalidationTree.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.modelix.model.sync.bulk
22

33
import org.modelix.model.api.IModel
4+
import org.modelix.model.api.INodeReference
45
import org.modelix.model.api.IReadableNode
56
import org.modelix.model.api.ITree
67
import org.modelix.model.api.NodeReference
@@ -27,12 +28,12 @@ class InvalidationTree(sizeLimit: Int = 100_000) : GenericInvalidationTree<Long,
2728
}
2829
}
2930

30-
class DefaultInvalidationTree(val root: NodeReference, sizeLimit: Int = 100_000) :
31-
GenericInvalidationTree<NodeReference, IModel>(root, sizeLimit = sizeLimit) {
31+
class DefaultInvalidationTree(val root: INodeReference, sizeLimit: Int = 100_000) :
32+
GenericInvalidationTree<INodeReference, IModel>(root, sizeLimit = sizeLimit) {
3233
override fun ancestorsAndSelf(
3334
model: IModel,
34-
nodeId: NodeReference,
35-
): List<NodeReference> {
35+
nodeId: INodeReference,
36+
): List<INodeReference> {
3637
return model.resolveNode(nodeId)
3738
?.ancestors(includeSelf = true)
3839
?.map { it.getNodeReference().toSerialized() }

datastructures/src/commonMain/kotlin/org/modelix/datastructures/patricia/PatriciaNode.kt

Lines changed: 121 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package org.modelix.datastructures.patricia
22

3+
import org.modelix.datastructures.EntryAddedEvent
4+
import org.modelix.datastructures.EntryChangedEvent
5+
import org.modelix.datastructures.EntryRemovedEvent
36
import org.modelix.datastructures.IPersistentMap
47
import org.modelix.datastructures.IPersistentMapRootData
8+
import org.modelix.datastructures.MapChangeEvent
59
import org.modelix.datastructures.objects.IObjectData
610
import org.modelix.datastructures.objects.IObjectDeserializer
711
import org.modelix.datastructures.objects.IObjectGraph
@@ -14,11 +18,18 @@ import org.modelix.datastructures.serialization.SplitJoinSerializer
1418
import org.modelix.kotlin.utils.urlDecode
1519
import org.modelix.kotlin.utils.urlEncode
1620
import org.modelix.streams.IStream
21+
import org.modelix.streams.flatten
22+
import org.modelix.streams.ifEmpty
1723
import org.modelix.streams.plus
1824

1925
data class PatriciaNode<K, V : Any>(
2026
val config: PatriciaTrieConfig<K, V>,
2127
val ownPrefix: String,
28+
29+
/**
30+
* Equivalent to `children.map { it.ownPrefix.first() }.join("")`, just allows choosing the correct child without
31+
* resolving all of them. Sorted.
32+
*/
2233
val firstChars: String,
2334
val children: List<ObjectReference<PatriciaNode<K, V>>>,
2435

@@ -72,6 +83,10 @@ data class PatriciaNode<K, V : Any>(
7283

7384
fun withValue(newValue: V?) = copy(value = newValue)
7485

86+
/**
87+
* Returns all entries.
88+
* @param prefix the prefix from the root to this node. Required to assemble the key.
89+
*/
7590
fun getEntries(prefix: CharSequence): IStream.Many<Pair<CharSequence, V>> {
7691
val fullPrefix = prefix + ownPrefix
7792
val descendants = IStream.Companion.many(children).flatMap { it.resolveData() }
@@ -135,40 +150,61 @@ data class PatriciaNode<K, V : Any>(
135150
}
136151
}
137152

138-
fun put(newKey: CharSequence, newValue: V?): IStream.ZeroOrOne<PatriciaNode<K, V>> {
153+
fun updateSubtree(newKey: CharSequence, updater: (PatriciaNode<K, V>) -> IStream.ZeroOrOne<PatriciaNode<K, V>>): IStream.ZeroOrOne<PatriciaNode<K, V>> {
139154
val commonPrefix = newKey.commonPrefixWith(this.ownPrefix)
140155
val remainingNewKey = newKey.drop(commonPrefix.length)
141156
val remainingOwnPrefix = this.ownPrefix.drop(commonPrefix.length)
142157
return (
143158
if (remainingOwnPrefix.isEmpty() && remainingNewKey.isEmpty()) {
144-
// key references this node -> overwrite the value
145-
IStream.of(withValue(newValue))
159+
// key references this node
160+
updater(this)
146161
} else if (remainingOwnPrefix.isEmpty()) {
147162
// key is longer -> insert into children
148163
val index = firstChars.binarySearch(remainingNewKey.first())
149164
if (index >= 0) {
150165
children[index].resolveData()
151-
.flatMapOne { it.put(remainingNewKey, newValue).orNull() }
166+
.flatMapOne { it.updateSubtree(remainingNewKey, updater).orNull() }
152167
.mapNotNull { withChildReplacedNullable(index, it) }
153168
} else {
154169
val insertionIndex = (-index) - 1
155-
val newChild = PatriciaNode<K, V>(
170+
PatriciaNode<K, V>(
156171
config = config,
157172
ownPrefix = remainingNewKey.toString(),
158-
value = newValue,
173+
value = null,
159174
firstChars = "",
160175
children = emptyList(),
161-
)
162-
IStream.of(withChildInserted(insertionIndex, remainingNewKey.first(), newChild))
176+
).let { updater(it) }
177+
.map { withChildInserted(insertionIndex, remainingNewKey.first(), it) }
178+
.ifEmpty {
179+
// updater returned an empty node, and since this part is about inserting, nothing changed
180+
this
181+
}
163182
}
164183
} else {
165184
// key is shorter -> need to split into a node with a shorter prefix
166-
split(commonPrefix).put(newKey, newValue)
185+
split(commonPrefix).updateSubtree(newKey, updater)
167186
}
168187
).flatMapZeroOrOne { it.tryMerge() }
169188
}
170189

171-
fun split(commonPrefix: CharSequence): PatriciaNode<K, V> {
190+
fun put(newKey: CharSequence, newValue: V?): IStream.ZeroOrOne<PatriciaNode<K, V>> {
191+
return updateSubtree(newKey) {
192+
IStream.of(it.copy(value = newValue))
193+
}
194+
}
195+
196+
fun replaceSubtree(prefix: CharSequence, newSubtree: PatriciaNode<K, V>?): IStream.ZeroOrOne<PatriciaNode<K, V>> {
197+
return updateSubtree(prefix) {
198+
IStream.ofNotNull(newSubtree?.copy(ownPrefix = it.ownPrefix))
199+
}
200+
}
201+
202+
/**
203+
* Shortens the prefix of this node to the given one as a preparation for inserting children or setting a value.
204+
*/
205+
private fun split(commonPrefix: CharSequence): PatriciaNode<K, V> {
206+
require(ownPrefix.startsWith(commonPrefix))
207+
if (ownPrefix.length == commonPrefix.length) return this
172208
val remainingPrefix = this.ownPrefix.drop(commonPrefix.length)
173209
return PatriciaNode<K, V>(
174210
config = config,
@@ -195,7 +231,7 @@ data class PatriciaNode<K, V : Any>(
195231
return ownPrefix.urlEncode() +
196232
S1 + firstChars.urlEncode() +
197233
S1 + children.joinToString(S2.toString()) { it.getHashString() } +
198-
S1 + value
234+
S1 + value?.let { config.valueConfig.serialize(it) }.urlEncode()
199235
}
200236

201237
override fun getDeserializer(): IObjectDeserializer<*> {
@@ -206,6 +242,79 @@ data class PatriciaNode<K, V : Any>(
206242
return children + (value?.let { config.valueConfig.getContainmentReferences(it) } ?: emptyList())
207243
}
208244

245+
fun getChanges(path: CharSequence, oldNode: PatriciaNode<K, V>?, changesOnly: Boolean): IStream.Many<MapChangeEvent<K, V>> {
246+
if (oldNode == null) {
247+
return if (changesOnly) {
248+
IStream.empty()
249+
} else {
250+
getEntries(path).map { EntryAddedEvent(config.keyConfig.deserialize(it.first.toString()), it.second) }
251+
}
252+
}
253+
return if (ownPrefix == oldNode.ownPrefix) {
254+
val pathForChildren = path + ownPrefix
255+
val matchingChildren = if (firstChars == oldNode.firstChars) {
256+
children.zip(oldNode.children)
257+
} else {
258+
val newChildren = firstChars.asSequence().zip(children.asSequence()).toMap()
259+
val oldChildren = oldNode.firstChars.asSequence().zip(oldNode.children.asSequence()).toMap()
260+
val allFirstChars = newChildren.keys.plus(oldChildren.keys).distinct()
261+
allFirstChars.map { newChildren[it] to oldChildren[it] }
262+
}
263+
val changesFromChildren = IStream.many(matchingChildren).flatMap { (newChildRef, oldChildRef) ->
264+
val newChild = newChildRef?.resolveData() ?: IStream.of(null)
265+
val oldChild = oldChildRef?.resolveData() ?: IStream.of(null)
266+
267+
newChild.zipWith(oldChild) { newChild, oldChild ->
268+
if (newChild == null) {
269+
if (oldChild == null) {
270+
IStream.empty()
271+
} else {
272+
if (changesOnly) {
273+
IStream.empty()
274+
} else {
275+
oldChild.getEntries(pathForChildren).map {
276+
EntryRemovedEvent(config.keyConfig.deserialize(it.first.toString()), it.second)
277+
}
278+
}
279+
}
280+
} else {
281+
newChild.getChanges(pathForChildren, oldChild, changesOnly)
282+
}
283+
}
284+
}.flatten()
285+
286+
fun ownKey() = config.keyConfig.deserialize(pathForChildren.toString())
287+
val ownChange = if (this.value == null) {
288+
if (oldNode.value == null) {
289+
IStream.empty()
290+
} else {
291+
IStream.of(EntryRemovedEvent(ownKey(), oldNode.value))
292+
}
293+
} else {
294+
if (oldNode.value == null) {
295+
IStream.of(EntryAddedEvent(ownKey(), this.value))
296+
} else {
297+
if (config.equal(this.value, oldNode.value)) {
298+
IStream.empty()
299+
} else {
300+
IStream.of(
301+
EntryChangedEvent(
302+
key = config.keyConfig.deserialize(pathForChildren.toString()),
303+
oldValue = oldNode.value,
304+
newValue = this.value,
305+
),
306+
)
307+
}
308+
}
309+
}
310+
311+
ownChange + changesFromChildren
312+
} else {
313+
val commonPrefix = ownPrefix.commonPrefixWith(oldNode.ownPrefix)
314+
split(commonPrefix).getChanges(path, oldNode.split(commonPrefix), changesOnly)
315+
}
316+
}
317+
209318
class Deserializer<K, V : Any>(val config: (IObjectGraph) -> PatriciaTrieConfig<K, V>) : IObjectDeserializer<PatriciaNode<K, V>> {
210319
constructor(config: PatriciaTrieConfig<K, V>) : this({ config })
211320
override fun deserialize(
@@ -221,7 +330,7 @@ data class PatriciaNode<K, V : Any>(
221330
parts[0].urlDecode()!!,
222331
parts[1].urlDecode()!!,
223332
parts[2].split(S2).filter { it.isNotEmpty() }.map { referenceFactory.fromHashString(it, this) },
224-
config.valueConfig.deserialize(parts[3]),
333+
parts[3].urlDecode()?.let { config.valueConfig.deserialize(it) },
225334
)
226335
}
227336
}

datastructures/src/commonMain/kotlin/org/modelix/datastructures/patricia/PatriciaTrie.kt

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,40 @@ class PatriciaTrie<K, V : Any>(
3636
return PatriciaTrie(config, (newRoot ?: PatriciaNode(config)).asObject(config.graph))
3737
}
3838

39+
private fun IStream.ZeroOrOne<PatriciaNode<K, V>>.withNewRoot() = orNull().map { withNewRoot(it) }
40+
3941
override fun get(key: K): IStream.ZeroOrOne<V> {
4042
return root.data.get(keyAsString(key))
4143
}
4244

4345
override fun put(key: K, value: V): IStream.One<PatriciaTrie<K, V>> {
4446
val keyString = keyAsString(key)
45-
return root.data.put(keyString, value).orNull().map { withNewRoot(it) }
47+
return root.data.put(keyString, value).withNewRoot()
4648
}
4749

50+
/**
51+
* Returns a copy of the tree that contains only those entries starting with the given prefix.
52+
*/
4853
fun slice(prefix: CharSequence): IStream.One<PatriciaTrie<K, V>> {
49-
return root.data.slice(prefix).orNull().map { withNewRoot(it) }
54+
return root.data.slice(prefix).withNewRoot()
55+
}
56+
57+
/**
58+
* Removes all entries starting with the given [prefix] and adds all entries from [newEntries] starting with the
59+
* same prefix.
60+
* This allows efficiently updating a whole subtree from an external source without having to compare the new and
61+
* existing data. It can just be reimported and then compared by using the builtin diff support of this
62+
* data structure.
63+
*/
64+
fun replaceSlice(prefix: CharSequence, newEntries: PatriciaTrie<K, V>): IStream.One<PatriciaTrie<K, V>> {
65+
if (prefix.isEmpty()) return IStream.of(newEntries)
66+
return newEntries.root.data.getSubtree(prefix).orNull().flatMapOne { replacement ->
67+
root.data.replaceSubtree(prefix, replacement).withNewRoot()
68+
}
5069
}
5170

5271
override fun remove(key: K): IStream.One<PatriciaTrie<K, V>> {
53-
return root.data.put(keyAsString(key), null).orNull().map { withNewRoot(it) }
72+
return root.data.put(keyAsString(key), null).withNewRoot()
5473
}
5574

5675
override fun getAllValues(): IStream.Many<V> {
@@ -66,7 +85,8 @@ class PatriciaTrie<K, V : Any>(
6685
}
6786

6887
override fun getAll(keys: Iterable<K>): IStream.Many<Pair<K, V>> {
69-
TODO("Not yet implemented")
88+
// TODO performance
89+
return IStream.many(keys).flatMap { key -> get(key).map { key to it } }
7090
}
7191

7292
override fun putAll(entries: Iterable<Pair<K, V>>): IStream.One<IPersistentMap<K, V>> {
@@ -75,7 +95,8 @@ class PatriciaTrie<K, V : Any>(
7595
}
7696

7797
override fun removeAll(keys: Iterable<K>): IStream.One<IPersistentMap<K, V>> {
78-
TODO("Not yet implemented")
98+
// TODO performance
99+
return keys.fold(IStream.of(this)) { acc, key -> acc.flatMapOne { it.remove(key) } }
79100
}
80101

81102
override fun removeAllEntries(entries: Iterable<Pair<K, V>>): IStream.One<IPersistentMap<K, V>> {
@@ -86,7 +107,8 @@ class PatriciaTrie<K, V : Any>(
86107
oldMap: IPersistentMap<K, V>,
87108
changesOnly: Boolean,
88109
): IStream.Many<MapChangeEvent<K, V>> {
89-
TODO("Not yet implemented")
110+
oldMap as PatriciaTrie<K, V>
111+
return root.data.getChanges("", oldMap.root.data, changesOnly)
90112
}
91113

92114
override fun equals(other: Any?): Boolean {

datastructures/src/commonMain/kotlin/org/modelix/datastructures/patricia/PatriciaTrieConfig.kt

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,29 @@ package org.modelix.datastructures.patricia
22

33
import org.modelix.datastructures.objects.IDataTypeConfiguration
44
import org.modelix.datastructures.objects.IObjectGraph
5+
import kotlin.jvm.JvmName
56

67
class PatriciaTrieConfig<K, V>(
78
val graph: IObjectGraph,
89
val keyConfig: IDataTypeConfiguration<K>,
910
val valueConfig: IDataTypeConfiguration<V>,
10-
)
11+
) {
12+
13+
@JvmName("keysEqual")
14+
fun equal(a: K, b: K): Boolean {
15+
return if (a == null) {
16+
b == null
17+
} else {
18+
b != null && keyConfig.equal(a, b)
19+
}
20+
}
21+
22+
@JvmName("valuesEqual")
23+
fun equal(a: V?, b: V?): Boolean {
24+
return if (a == null) {
25+
b == null
26+
} else {
27+
b != null && valueConfig.equal(a, b)
28+
}
29+
}
30+
}

0 commit comments

Comments
 (0)