Skip to content

Commit c97a190

Browse files
committed
feat(model-server): validation of operations in a version
The model server now executes the operations defined a version and checks if they actually produce the new model referenced in that version. This is important for not losing changes during a merge, because a merge is done by simply re-executing these operations in a different order.
1 parent 53f2f02 commit c97a190

File tree

58 files changed

+946
-504
lines changed

Some content is hidden

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

58 files changed

+946
-504
lines changed

bulk-model-sync-lib/src/commonMain/kotlin/org/modelix/model/sync/bulk/IdentityPreservingNodeAssociation.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class IdentityPreservingNodeAssociation(
1717
override fun resolveTarget(sourceNode: IReadableNode): IWritableNode? {
1818
val sourceReference = sourceNode.getNodeReference()
1919
val targetReference = overrides[sourceReference] ?: sourceReference
20-
return targetModel.resolveNode(targetReference)
20+
return targetModel.tryResolveNode(targetReference)
2121
}
2222

2323
override fun associate(

bulk-model-sync-lib/src/commonMain/kotlin/org/modelix/model/sync/bulk/NodeAssociationToModelServer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class NodeAssociationFromModelServer(val branch: IBranch, val targetModel: IMuta
6161

6262
override fun resolveTarget(sourceNode: IReadableNode): IWritableNode? {
6363
return (pendingAssociations[nodeId(sourceNode)] ?: sourceNode.getOriginalReference())
64-
?.let { targetModel.resolveNode(NodeReference(it)) }
64+
?.let { targetModel.tryResolveNode(NodeReference(it)) }
6565
}
6666

6767
override fun associate(sourceNode: IReadableNode, targetNode: IWritableNode) {

bulk-model-sync-lib/src/commonMain/kotlin/org/modelix/model/sync/bulk/TransientNodeAssociation.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class TransientNodeAssociation(val writeOriginalIds: Boolean, val targetModel: I
3030
override fun resolveTarget(sourceNode: IReadableNode): IWritableNode? {
3131
val ref = sourceNode.getOriginalOrCurrentReference()
3232
return associations[ref]
33-
?: targetModel.resolveNode(NodeReference(ref))
33+
?: targetModel.tryResolveNode(NodeReference(ref))
3434
}
3535

3636
override fun associate(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class DefaultInvalidationTree(val root: INodeReference, sizeLimit: Int = 100_000
3434
model: IModel,
3535
nodeId: INodeReference,
3636
): List<INodeReference> {
37-
return model.resolveNode(nodeId)
37+
return model.tryResolveNode(nodeId)
3838
?.ancestors(includeSelf = true)
3939
?.map { it.getNodeReference().toSerialized() }
4040
?.toList()

model-api/src/commonMain/kotlin/org/modelix/model/api/IModel.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,25 @@ import org.modelix.model.area.IAreaReference
77
interface IModel : ITransactionManager {
88
fun getRootNode(): IReadableNode
99
fun getRootNodes(): List<IReadableNode>
10-
fun resolveNode(ref: INodeReference): IReadableNode?
10+
fun tryResolveNode(ref: INodeReference): IReadableNode?
11+
fun resolveNode(ref: INodeReference): IReadableNode = tryResolveNode(ref) ?: throw NodeNotFoundException(ref, this)
1112
}
1213

1314
interface IMutableModel : IModel {
1415
fun asArea(): IArea = ModelAsArea(this)
1516
override fun getRootNode(): IWritableNode
1617
override fun getRootNodes(): List<IWritableNode>
17-
override fun resolveNode(ref: INodeReference): IWritableNode?
18+
override fun tryResolveNode(ref: INodeReference): IWritableNode?
19+
override fun resolveNode(ref: INodeReference): IWritableNode {
20+
return super.resolveNode(ref) as IWritableNode
21+
}
1822
}
1923

2024
data class AreaAsModel(val area: IArea) : IMutableModel {
2125
override fun asArea(): IArea = area
2226
override fun getRootNode(): IWritableNode = area.getRoot().asWritableNode()
2327
override fun getRootNodes(): List<IWritableNode> = listOf(getRootNode())
24-
override fun resolveNode(ref: INodeReference): IWritableNode? = area.resolveNode(ref)?.asWritableNode()
28+
override fun tryResolveNode(ref: INodeReference): IWritableNode? = area.resolveNode(ref)?.asWritableNode()
2529
override fun <R> executeRead(body: () -> R): R = area.executeRead(body)
2630
override fun <R> executeWrite(body: () -> R): R = area.executeWrite(body)
2731
override fun canRead(): Boolean = area.canRead()
@@ -35,7 +39,7 @@ data class ModelAsArea(val model: IMutableModel) : IArea, IAreaReference {
3539

3640
override fun resolveConcept(ref: IConceptReference): IConcept? = ILanguageRepository.resolveConcept(ref)
3741

38-
override fun resolveNode(ref: INodeReference): INode? = model.resolveNode(ref)?.asLegacyNode()
42+
override fun resolveNode(ref: INodeReference): INode? = model.tryResolveNode(ref)?.asLegacyNode()
3943

4044
override fun resolveOriginalNode(ref: INodeReference): INode? = resolveNode(ref)
4145

@@ -63,3 +67,5 @@ data class ModelAsArea(val model: IMutableModel) : IArea, IAreaReference {
6367
throw UnsupportedOperationException()
6468
}
6569
}
70+
71+
class NodeNotFoundException(nodeRef: INodeReference, model: IModel) : NoSuchElementException("Node not found: $nodeRef")

model-api/src/commonMain/kotlin/org/modelix/model/api/LocalPNodeReference.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ data class LocalPNodeReference(val id: Long) : INodeReference() {
1414
return id.toULong().toString(16)
1515
}
1616

17-
override fun toString(): String {
18-
return "LocalPNodeReference_${id.toString(16)}"
17+
companion object {
18+
fun tryConvert(ref: INodeReference): LocalPNodeReference? {
19+
if (ref is LocalPNodeReference) return ref
20+
return ref.serialize().toULongOrNull(16)?.let { LocalPNodeReference(it.toLong()) }
21+
}
1922
}
2023
}

model-api/src/commonMain/kotlin/org/modelix/model/api/PNodeReference.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@ data class PNodeReference(val id: Long, val treeId: String) : INodeReference() {
1616
return "modelix:$branchId/${id.toString(16)}"
1717
}
1818

19-
override fun toString(): String {
20-
return serialize()
21-
}
22-
2319
companion object {
2420
fun deserialize(serialized: String): PNodeReference {
2521
return requireNotNull(tryDeserialize(serialized)) {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,11 @@ class DefaultModelTree(nodesMap: IPersistentMap<INodeReference, NodeObjectData<I
5959
class NodeNotFoundException(nodeId: Any?) : RuntimeException("Node doesn't exist: $nodeId")
6060

6161
@JvmName("asModelTreeWithNodeReferences")
62-
fun IPersistentMap<INodeReference, NodeObjectData<INodeReference>>.asModelTree(treeId: TreeId): IModelTree<INodeReference> {
62+
fun IPersistentMap<INodeReference, NodeObjectData<INodeReference>>.asModelTree(treeId: TreeId): IGenericModelTree<INodeReference> {
6363
return DefaultModelTree(this, treeId)
6464
}
6565

6666
@JvmName("asModelTreeWithInt64")
67-
fun IPersistentMap<Long, NodeObjectData<Long>>.asModelTree(treeId: TreeId): IModelTree<Long> {
67+
fun IPersistentMap<Long, NodeObjectData<Long>>.asModelTree(treeId: TreeId): IGenericModelTree<Long> {
6868
return Int64ModelTree(this, treeId)
6969
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import org.modelix.streams.plus
2424
abstract class GenericModelTree<NodeId>(
2525
val nodesMap: IPersistentMap<NodeId, NodeObjectData<NodeId>>,
2626
private val treeId: TreeId,
27-
) : IModelTree<NodeId>, IStreamExecutorProvider by nodesMap {
27+
) : IGenericModelTree<NodeId>, IStreamExecutorProvider by nodesMap {
2828
protected abstract fun withNewMap(newNodesMap: IPersistentMap<NodeId, NodeObjectData<NodeId>>): GenericModelTree<NodeId>
2929
abstract override fun getRootNodeId(): NodeId
3030
protected fun toGlobalNodeReference(ref: INodeReference): INodeReference = when (ref) {
@@ -117,7 +117,7 @@ abstract class GenericModelTree<NodeId>(
117117
}
118118
}
119119

120-
override fun getChanges(oldVersion: IModelTree<NodeId>, changesOnly: Boolean): IStream.Many<ModelChangeEvent<NodeId>> {
120+
override fun getChanges(oldVersion: IGenericModelTree<NodeId>, changesOnly: Boolean): IStream.Many<ModelChangeEvent<NodeId>> {
121121
require(oldVersion is GenericModelTree<NodeId>)
122122
if (nodesMap.asObject().getHash() == oldVersion.nodesMap.asObject().getHash()) return IStream.empty()
123123
return getChanges(oldVersion, nodesMap, oldVersion.nodesMap, changesOnly)

model-datastructure/src/commonMain/kotlin/org/modelix/datastructures/model/IModelTree.kt renamed to model-datastructure/src/commonMain/kotlin/org/modelix/datastructures/model/IGenericModelTree.kt

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import org.modelix.streams.IStream
1313
import org.modelix.streams.IStreamExecutorProvider
1414
import org.modelix.streams.plus
1515

16+
typealias IModelTree = IGenericModelTree<INodeReference>
17+
1618
/**
1719
* Persistent model implementation that supports bulk requests, bulk updates and non-Long node IDs.
1820
* Consistently using the streams API enables efficient lazy loading of model data.
@@ -22,14 +24,14 @@ import org.modelix.streams.plus
2224
* as the primary ID. This makes importing and updating models from source such as MPS easier and more efficient. No
2325
* need for maintaining a mapping between the MPS ID and the Modelix ID. Also, since there is no generated ID that has
2426
* a different value for each import, we don't have to check if a node already exists and reuse it. We can just run a
25-
* fresh import and then do a diff. All the data stored in the [IModelTree] also exists in the original source and if it
27+
* fresh import and then do a diff. All the data stored in the [IGenericModelTree] also exists in the original source and if it
2628
* didn't change, we will end up with the same hash.
2729
* With a prefix tree like the [org.modelix.datastructures.patricia.PatriciaTrie] we can even re-run the import for
2830
* individual MPS models very efficiently, because all the nodes in the same model have the same model ID as a common
2931
* prefix of their node ID and will end up in the same subtree of the trie data structure, giving us a single hash for
3032
* the imported MPS model.
3133
*/
32-
interface IModelTree<NodeId> : IStreamExecutorProvider {
34+
interface IGenericModelTree<NodeId> : IStreamExecutorProvider {
3335
fun asObject(): Object<CPTree>
3436
fun getNodeIdType(): IDataTypeConfiguration<NodeId>
3537

@@ -57,17 +59,19 @@ interface IModelTree<NodeId> : IStreamExecutorProvider {
5759
fun getChildRoles(parentId: NodeId): IStream.Many<IChildLinkReference>
5860
fun getChildrenAndRoles(parentId: NodeId): IStream.Many<Pair<IChildLinkReference, IStream.Many<NodeId>>>
5961

60-
fun getChanges(oldVersion: IModelTree<NodeId>, changesOnly: Boolean): IStream.Many<ModelChangeEvent<NodeId>>
62+
fun getChanges(oldVersion: IGenericModelTree<NodeId>, changesOnly: Boolean): IStream.Many<ModelChangeEvent<NodeId>>
6163

62-
fun mutate(operations: Iterable<MutationParameters<NodeId>>): IStream.One<IModelTree<NodeId>>
64+
fun mutate(operations: Iterable<MutationParameters<NodeId>>): IStream.One<IGenericModelTree<NodeId>>
6365

64-
fun mutate(operation: MutationParameters<NodeId>): IStream.One<IModelTree<NodeId>> = mutate(listOf(operation))
66+
fun mutate(operation: MutationParameters<NodeId>): IStream.One<IGenericModelTree<NodeId>> = mutate(listOf(operation))
6567

6668
companion object {
6769
fun builder(): ModelTreeBuilder<Long> = ModelTreeBuilder.newWithInt64Ids()
6870
}
6971
}
7072

73+
fun IGenericModelTree<*>.getHash() = asObject().getHash()
74+
7175
sealed class MutationParameters<NodeId> {
7276
sealed class Node<NodeId> : MutationParameters<NodeId>() {
7377
abstract val nodeId: NodeId
@@ -112,36 +116,36 @@ sealed class MutationParameters<NodeId> {
112116
data class Remove<NodeId>(override val nodeId: NodeId) : Node<NodeId>()
113117
}
114118

115-
fun <NodeId> IModelTree<NodeId>.getDescendants(nodeId: NodeId, includeSelf: Boolean): IStream.Many<NodeId> {
119+
fun <NodeId> IGenericModelTree<NodeId>.getDescendants(nodeId: NodeId, includeSelf: Boolean): IStream.Many<NodeId> {
116120
return if (includeSelf) {
117121
IStream.of(nodeId).plus(getDescendants(nodeId, false))
118122
} else {
119123
getChildren(nodeId).flatMap { getDescendants(it, true) }
120124
}
121125
}
122126

123-
fun <NodeId> IModelTree<NodeId>.getAncestors(nodeId: NodeId, includeSelf: Boolean): IStream.Many<NodeId> {
127+
fun <NodeId> IGenericModelTree<NodeId>.getAncestors(nodeId: NodeId, includeSelf: Boolean): IStream.Many<NodeId> {
124128
return if (includeSelf) {
125129
IStream.of(nodeId).plus(getAncestors(nodeId, false))
126130
} else {
127131
getParent(nodeId).flatMap { getAncestors(it, true) }
128132
}
129133
}
130134

131-
fun <NodeId> IModelTree<NodeId>.setProperty(nodeId: NodeId, role: IPropertyReference, value: String?): IStream.One<IModelTree<NodeId>> =
135+
fun <NodeId> IGenericModelTree<NodeId>.setProperty(nodeId: NodeId, role: IPropertyReference, value: String?): IStream.One<IGenericModelTree<NodeId>> =
132136
mutate(MutationParameters.Property(nodeId, role, value))
133137

134-
fun <NodeId> IModelTree<NodeId>.setReferenceTarget(sourceId: NodeId, role: IReferenceLinkReference, target: INodeReference): IStream.One<IModelTree<NodeId>> =
138+
fun <NodeId> IGenericModelTree<NodeId>.setReferenceTarget(sourceId: NodeId, role: IReferenceLinkReference, target: INodeReference): IStream.One<IGenericModelTree<NodeId>> =
135139
mutate(MutationParameters.Reference(sourceId, role, target))
136140

137-
fun <NodeId> IModelTree<NodeId>.moveNode(newParentId: NodeId, newRole: IChildLinkReference, newIndex: Int, childId: NodeId): IStream.One<IModelTree<NodeId>> =
141+
fun <NodeId> IGenericModelTree<NodeId>.moveNode(newParentId: NodeId, newRole: IChildLinkReference, newIndex: Int, childId: NodeId): IStream.One<IGenericModelTree<NodeId>> =
138142
mutate(MutationParameters.Move(newParentId, newRole, newIndex, listOf(childId)))
139143

140-
fun <NodeId> IModelTree<NodeId>.addNewChild(parentId: NodeId, role: IChildLinkReference, index: Int, childId: NodeId, concept: ConceptReference): IStream.One<IModelTree<NodeId>> =
144+
fun <NodeId> IGenericModelTree<NodeId>.addNewChild(parentId: NodeId, role: IChildLinkReference, index: Int, childId: NodeId, concept: ConceptReference): IStream.One<IGenericModelTree<NodeId>> =
141145
mutate(MutationParameters.AddNew(parentId, role, index, listOf(childId to concept)))
142146

143-
fun <NodeId> IModelTree<NodeId>.removeNode(nodeId: NodeId): IStream.One<IModelTree<NodeId>> =
147+
fun <NodeId> IGenericModelTree<NodeId>.removeNode(nodeId: NodeId): IStream.One<IGenericModelTree<NodeId>> =
144148
mutate(MutationParameters.Remove(nodeId))
145149

146-
fun <NodeId> IModelTree<NodeId>.changeConcept(nodeId: NodeId, concept: ConceptReference): IStream.One<IModelTree<NodeId>> =
150+
fun <NodeId> IGenericModelTree<NodeId>.changeConcept(nodeId: NodeId, concept: ConceptReference): IStream.One<IGenericModelTree<NodeId>> =
147151
mutate(MutationParameters.Concept(nodeId, concept))

0 commit comments

Comments
 (0)