Skip to content

Commit 203841b

Browse files
committed
fix(model-client): IModelClientV2.migrateRoles
1 parent 403471f commit 203841b

File tree

7 files changed

+98
-67
lines changed

7 files changed

+98
-67
lines changed

model-client/src/commonMain/kotlin/org/modelix/model/client2/RolesMigration.kt

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package org.modelix.model.client2
22

3+
import org.modelix.datastructures.model.DefaultModelTree
4+
import org.modelix.datastructures.model.IGenericModelTree
5+
import org.modelix.datastructures.model.Int64ModelTree
36
import org.modelix.datastructures.model.getDescendants
47
import org.modelix.datastructures.objects.asObject
58
import org.modelix.kotlin.utils.DelicateModelixApi
@@ -17,6 +20,7 @@ import org.modelix.model.lazy.BranchReference
1720
import org.modelix.model.lazy.CLVersion
1821
import org.modelix.model.lazy.runWriteOnTree
1922
import org.modelix.model.mutable.DummyIdGenerator
23+
import org.modelix.model.mutable.VersionedModelTree
2024
import org.modelix.model.mutable.moveChildren
2125
import org.modelix.model.mutable.setProperty
2226
import org.modelix.model.mutable.setReferenceTarget
@@ -50,6 +54,10 @@ suspend fun IModelClientV2.migrateRoles(
5054
},
5155
)
5256

57+
/**
58+
* Ensure this executed without concurrently active clients so that no merges can happen.
59+
*/
60+
@OptIn(DelicateModelixApi::class)
5361
suspend fun IModelClientV2.migrateRoles(
5462
branch: BranchReference,
5563
roleReplacement: IRoleReplacement,
@@ -65,41 +73,45 @@ suspend fun IModelClientV2.migrateRoles(
6573
),
6674
)
6775

68-
val newVersion = oldVersion.runWriteOnTree(nodeIdGenerator = DummyIdGenerator<INodeReference>(), getUserId()) { newTree ->
69-
val oldTree = newTree.getTransaction().tree
70-
oldTree.getDescendants(oldTree.getRootNodeId(), true)
71-
.flatMap { IStream.of(it).zipWith(oldTree.getConceptReference(it)) { nodeId, concept -> nodeId to concept } }
72-
.flatMap { (nodeId, concept) ->
73-
oldTree.getChildren(nodeId).flatMap { childId ->
74-
IStream.of(childId)
75-
.zipWith(oldTree.getRoleInParent(childId).firstOrDefault { NullChildLinkReference }) { id, role -> id to role }
76-
}.toList().forEach { children ->
77-
val childrenByRole = children.groupBy { it.second }
78-
for ((oldRole, childrenInOldRole) in childrenByRole) {
79-
val newRole = if (oldRole.matches(NullChildLinkReference)) {
80-
NullChildLinkReference
81-
} else {
82-
roleReplacement.replaceRole(concept, oldRole)
76+
val newVersion = oldVersion
77+
.runWriteOnTree(nodeIdGenerator = DummyIdGenerator<INodeReference>(), getUserId()) { newTree ->
78+
val oldTree = newTree.getTransaction().tree
79+
80+
(newTree.getWriteTransaction() as VersionedModelTree.VersionedWriteTransaction).unsafeSetTree(newTree.getTransaction().tree.withUseRoleIds(true))
81+
82+
oldTree.getDescendants(oldTree.getRootNodeId(), true)
83+
.flatMap { IStream.of(it).zipWith(oldTree.getConceptReference(it)) { nodeId, concept -> nodeId to concept } }
84+
.flatMap { (nodeId, concept) ->
85+
oldTree.getChildren(nodeId).flatMap { childId ->
86+
IStream.of(childId)
87+
.zipWith(oldTree.getRoleInParent(childId).firstOrDefault { NullChildLinkReference }) { id, role -> id to role }
88+
}.toList().forEach { children ->
89+
val childrenByRole = children.groupBy { it.second }
90+
for ((oldRole, childrenInOldRole) in childrenByRole) {
91+
val newRole = if (oldRole.matches(NullChildLinkReference)) {
92+
NullChildLinkReference
93+
} else {
94+
roleReplacement.replaceRole(concept, oldRole)
95+
}
96+
if (oldRole == newRole) continue
97+
newTree.getWriteTransaction().moveChildren(nodeId, newRole, -1, childrenInOldRole.map { it.first })
8398
}
84-
if (oldRole == newRole) continue
85-
newTree.getWriteTransaction().moveChildren(nodeId, newRole, -1, childrenInOldRole.map { it.first })
86-
}
87-
}.andThen(
88-
oldTree.getProperties(nodeId).forEach { (oldRole, value) ->
89-
val newRole = roleReplacement.replaceRole(concept, oldRole)
90-
if (oldRole == newRole) return@forEach
91-
newTree.setProperty(nodeId, oldRole, null)
92-
newTree.setProperty(nodeId, newRole, value)
9399
}.andThen(
94-
oldTree.getReferenceTargets(nodeId).forEach { (oldRole, value) ->
100+
oldTree.getProperties(nodeId).forEach { (oldRole, value) ->
95101
val newRole = roleReplacement.replaceRole(concept, oldRole)
96-
newTree.setReferenceTarget(nodeId, oldRole, null)
97-
newTree.setReferenceTarget(nodeId, newRole, value)
98-
},
99-
),
100-
).andThenUnit()
101-
}.drainAll().executeBlocking(oldTree)
102-
}.replaceMainTree { it.copy(usesRoleIds = true) }
102+
if (oldRole == newRole) return@forEach
103+
newTree.setProperty(nodeId, oldRole, null)
104+
newTree.setProperty(nodeId, newRole, value)
105+
}.andThen(
106+
oldTree.getReferenceTargets(nodeId).forEach { (oldRole, value) ->
107+
val newRole = roleReplacement.replaceRole(concept, oldRole)
108+
newTree.setReferenceTarget(nodeId, oldRole, null)
109+
newTree.setReferenceTarget(nodeId, newRole, value)
110+
},
111+
),
112+
).andThenUnit()
113+
}.drainAll().executeBlocking(oldTree)
114+
}
103115

104116
push(branch, newVersion, oldVersion)
105117
return newVersion
@@ -123,3 +135,11 @@ private fun CPVersion.replaceMainTree(modifier: (CPTree) -> CPTree): CPVersion {
123135
val newRef = mainTreeRef.graph.fromCreated(newData)
124136
return copy(treeRefs = treeRefs + (TreeType.MAIN to newRef))
125137
}
138+
139+
fun <NodeId> IGenericModelTree<*>.withUseRoleIds(useRoleIds: Boolean): IGenericModelTree<NodeId> {
140+
return when (this) {
141+
is Int64ModelTree -> Int64ModelTree(this.nodesMap, this.getId(), useRoleIds)
142+
is DefaultModelTree -> DefaultModelTree(this.nodesMap, this.getId(), useRoleIds)
143+
else -> throw IllegalArgumentException("unknown tree type: $this")
144+
} as IGenericModelTree<NodeId>
145+
}

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

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import org.modelix.model.api.IChildLinkReference
1111
import org.modelix.model.api.INodeReference
1212
import org.modelix.model.api.IPropertyReference
1313
import org.modelix.model.api.IReferenceLinkReference
14+
import org.modelix.model.api.IRoleReference
1415
import org.modelix.model.api.LocalPNodeReference
1516
import org.modelix.model.api.PNodeReference
1617
import org.modelix.model.api.meta.NullConcept
@@ -202,8 +203,8 @@ abstract class GenericModelTree<NodeId>(
202203
val childrenChanges: IStream.Many<ChildrenChangedEvent<NodeId>> = newChildren.zipWith(oldChildren) { newChildrenList, oldChildrenList ->
203204
val oldChildren: MutableMap<String?, MutableList<NodeObjectData<NodeId>>> = HashMap()
204205
val newChildren: MutableMap<String?, MutableList<NodeObjectData<NodeId>>> = HashMap()
205-
oldChildrenList.forEach { oldChildren.getOrPut(it.containment?.second?.getIdOrNameOrNull(), { ArrayList() }).add(it) }
206-
newChildrenList.forEach { newChildren.getOrPut(it.containment?.second?.getIdOrNameOrNull(), { ArrayList() }).add(it) }
206+
oldChildrenList.forEach { oldChildren.getOrPut(it.containment?.second, { ArrayList() }).add(it) }
207+
newChildrenList.forEach { newChildren.getOrPut(it.containment?.second, { ArrayList() }).add(it) }
207208

208209
val roles: MutableSet<String?> = HashSet()
209210
roles.addAll(oldChildren.keys)
@@ -299,11 +300,10 @@ abstract class GenericModelTree<NodeId>(
299300
): IStream.One<IPersistentMap<NodeId, NodeObjectData<NodeId>>> {
300301
val newNodes = newIds.zip(concepts).map { (childId, concept) ->
301302
childId to NodeObjectData<NodeId>(
302-
deserializer = NodeObjectData.Deserializer(graph, this.nodesMap.getKeyTypeConfig(), getId(), useRoleIds = useRoleIds),
303+
deserializer = NodeObjectData.Deserializer(graph, this.nodesMap.getKeyTypeConfig(), getId()),
303304
id = childId,
304305
concept = concept.takeIf { it != NullConcept.getReference() },
305-
containment = parentId to role,
306-
useRoleIds = useRoleIds,
306+
containment = parentId to role.chooseIdOrNameOrNull(),
307307
)
308308
}
309309

@@ -346,11 +346,11 @@ abstract class GenericModelTree<NodeId>(
346346
.exceptionIfEmpty { NodeNotFoundException(nodesMap.getKeyTypeConfig().serialize(id)) }
347347

348348
private fun setPropertyValue(nodeId: NodeId, role: IPropertyReference, value: String?): IStream.One<IPersistentMap<NodeId, NodeObjectData<NodeId>>> {
349-
return updateNode(nodeId) { IStream.of(it.withPropertyValue(role, value)) }
349+
return updateNode(nodeId) { IStream.of(it.withPropertyValue(role, useRoleIds = useRoleIds, value)) }
350350
}
351351

352352
private fun setReferenceTarget(sourceId: NodeId, role: IReferenceLinkReference, target: INodeReference?): IStream.One<IPersistentMap<NodeId, NodeObjectData<NodeId>>> {
353-
return updateNode(sourceId) { IStream.of(it.withReferenceTarget(role, target)) }
353+
return updateNode(sourceId) { IStream.of(it.withReferenceTarget(role, useRoleIds = useRoleIds, target)) }
354354
}
355355

356356
private fun moveChild(
@@ -398,7 +398,7 @@ abstract class GenericModelTree<NodeId>(
398398
}.wrap()
399399
val withUpdatedRole = withChildAdded.flatMapOne { tree ->
400400
tree.updateNode(childId) {
401-
IStream.of(it.copy(containment = newParentId to newRole))
401+
IStream.of(it.copy(containment = newParentId to newRole.chooseIdOrNameOrNull()))
402402
}
403403
}
404404
withUpdatedRole
@@ -423,4 +423,7 @@ abstract class GenericModelTree<NodeId>(
423423
updateNodeInMap(mapWithoutRemovedNodes, parentId) { it.withChildRemoved(nodeId) }
424424
}.flatten().wrap()
425425
}
426+
427+
private fun IRoleReference.chooseIdOrName() = if (useRoleIds) getIdOrName() else getNameOrId()
428+
private fun IChildLinkReference.chooseIdOrNameOrNull() = if (useRoleIds) getIdOrNameOrNull() else getNameOrIdOrNull()
426429
}

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,16 @@ abstract class ModelTreeBuilder<NodeId> private constructor(protected val common
2828
override fun build(): IGenericModelTree<Long> {
2929
val nodeIdType = LongDataTypeConfiguration()
3030
val root = NodeObjectData<Long>(
31-
deserializer = NodeObjectData.Deserializer(common.graph, nodeIdType, common.treeId, useRoleIds = common.storeRoleIds),
31+
deserializer = NodeObjectData.Deserializer(common.graph, nodeIdType, common.treeId),
3232
id = ITree.ROOT_ID,
3333
concept = null,
3434
containment = null,
35-
useRoleIds = common.storeRoleIds,
3635
).asObject(common.graph)
3736

3837
val config = HamtNode.Config(
3938
graph = common.graph,
4039
keyConfig = nodeIdType,
41-
valueConfig = ObjectReferenceDataTypeConfiguration(common.graph, NodeObjectData.Deserializer(common.graph, nodeIdType, common.treeId, useRoleIds = common.storeRoleIds)),
40+
valueConfig = ObjectReferenceDataTypeConfiguration(common.graph, NodeObjectData.Deserializer(common.graph, nodeIdType, common.treeId)),
4241
)
4342
return HamtInternalNode.createEmpty(config)
4443
.put(root.data.id, root.ref, common.graph)
@@ -53,16 +52,15 @@ abstract class ModelTreeBuilder<NodeId> private constructor(protected val common
5352
override fun build(): IGenericModelTree<INodeReference> {
5453
val nodeIdType = NodeReferenceDataTypeConfig()
5554
val root = NodeObjectData<INodeReference>(
56-
deserializer = NodeObjectData.Deserializer(common.graph, nodeIdType, common.treeId, useRoleIds = common.storeRoleIds),
55+
deserializer = NodeObjectData.Deserializer(common.graph, nodeIdType, common.treeId),
5756
id = PNodeReference(ITree.ROOT_ID, common.treeId.id),
5857
concept = null,
5958
containment = null,
60-
useRoleIds = common.storeRoleIds,
6159
).asObject(common.graph)
6260
val config = PatriciaTrieConfig(
6361
graph = common.graph,
6462
keyConfig = nodeIdType,
65-
valueConfig = ObjectReferenceDataTypeConfiguration(common.graph, NodeObjectData.Deserializer(common.graph, nodeIdType, common.treeId, useRoleIds = common.storeRoleIds)),
63+
valueConfig = ObjectReferenceDataTypeConfiguration(common.graph, NodeObjectData.Deserializer(common.graph, nodeIdType, common.treeId)),
6664
)
6765
return PatriciaTrie(config)
6866
.put(root.data.id, root.ref)

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

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,22 +35,26 @@ data class NodeObjectData<NodeId>(
3535
val deserializer: Deserializer<NodeId>? = null,
3636
val id: NodeId,
3737
val concept: ConceptReference? = null,
38-
val containment: Pair<NodeId, IChildLinkReference>? = null,
38+
val containment: Pair<NodeId, String?>? = null,
3939
val children: LargeList<NodeId>? = null,
4040
val properties: List<Pair<String, String>> = emptyList(),
4141
val references: List<Pair<String, INodeReference>> = emptyList(),
42-
val useRoleIds: Boolean,
4342
) : IObjectData {
4443

4544
init {
45+
// ensure consistent encoding nullable values
4646
require(concept == null || concept.getUID() != NullConcept.getUID())
47-
require(containment == null || containment.second == NullChildLinkReference || containment.second.getIdOrNameOrNull() != "null")
47+
require(
48+
containment == null || containment.second == null || !IChildLinkReference.fromString(containment.second).matches(
49+
NullChildLinkReference,
50+
),
51+
)
4852
}
4953

5054
val parentId: NodeId? get() = containment?.first
51-
val roleInParent: IChildLinkReference get() = containment?.second ?: NullChildLinkReference
55+
val roleInParent: IChildLinkReference get() = containment?.second?.let { IChildLinkReference.fromString(it) } ?: NullChildLinkReference
5256

53-
private fun getRoleInParentAsString(): String? = roleInParent.chooseIdOrNameOrNull()
57+
private fun getRoleInParentAsString(): String? = containment?.second
5458

5559
fun getChildIds(): IStream.Many<NodeId> = children?.getElements() ?: IStream.empty()
5660

@@ -74,7 +78,7 @@ data class NodeObjectData<NodeId>(
7478
return references.find { role.matches(it.first) }?.second
7579
}
7680

77-
fun withPropertyValue(role: IPropertyReference, value: String?): NodeObjectData<NodeId> {
81+
fun withPropertyValue(role: IPropertyReference, useRoleIds: Boolean, value: String?): NodeObjectData<NodeId> {
7882
var index = properties.indexOfFirst { role.matches(it.first) }
7983
return if (value == null) {
8084
if (index < 0) {
@@ -86,16 +90,16 @@ data class NodeObjectData<NodeId>(
8690
// persist ID only to prevent ObjectHash changes when metamodel elements are renamed
8791
@OptIn(DelicateModelixApi::class)
8892
val newProperties = if (index < 0) {
89-
properties + (role.chooseIdOrName() to value)
93+
properties + (role.chooseIdOrName(useRoleIds) to value)
9094
} else {
91-
properties.take(index) + (role.chooseIdOrName() to value) + properties.drop(index + 1)
95+
properties.take(index) + (role.chooseIdOrName(useRoleIds) to value) + properties.drop(index + 1)
9296
}
9397
// sorted to get a stable ObjectHash and avoid non-determinism in algorithms working with the model (e.g. sync)
9498
copy(properties = newProperties.sortedBy { it.first })
9599
}
96100
}
97101

98-
fun withReferenceTarget(role: IReferenceLinkReference, value: INodeReference?): NodeObjectData<NodeId> {
102+
fun withReferenceTarget(role: IReferenceLinkReference, useRoleIds: Boolean, value: INodeReference?): NodeObjectData<NodeId> {
99103
var index = references.indexOfFirst { role.matches(it.first) }
100104
return if (value == null) {
101105
if (index < 0) {
@@ -107,17 +111,17 @@ data class NodeObjectData<NodeId>(
107111
// persist ID only to prevent ObjectHash changes when metamodel elements are renamed
108112
@OptIn(DelicateModelixApi::class)
109113
val newReferences = if (index < 0) {
110-
references + (role.chooseIdOrName() to value)
114+
references + (role.chooseIdOrName(useRoleIds) to value)
111115
} else {
112-
references.take(index) + (role.chooseIdOrName() to value) + references.drop(index + 1)
116+
references.take(index) + (role.chooseIdOrName(useRoleIds) to value) + references.drop(index + 1)
113117
}
114118
// sorted to get a stable ObjectHash and avoid non-determinism in algorithms working with the model (e.g. sync)
115119
copy(references = newReferences.sortedBy { it.first })
116120
}
117121
}
118122

119-
private fun IRoleReference.chooseIdOrName() = if (useRoleIds) getIdOrName() else getNameOrId()
120-
private fun IChildLinkReference.chooseIdOrNameOrNull() = if (useRoleIds) getIdOrNameOrNull() else getNameOrIdOrNull()
123+
private fun IRoleReference.chooseIdOrName(useRoleIds: Boolean) = if (useRoleIds) getIdOrName() else getNameOrId()
124+
private fun IChildLinkReference.chooseIdOrNameOrNull(useRoleIds: Boolean) = if (useRoleIds) getIdOrNameOrNull() else getNameOrIdOrNull()
121125

122126
fun withChildRemoved(childId: NodeId): IStream.One<NodeObjectData<NodeId>> {
123127
return getChildIds().filter { !deserializer!!.nodeIdTypeConfig.equal(it, childId) }.toList().map { newChildren ->
@@ -135,7 +139,6 @@ data class NodeObjectData<NodeId>(
135139
val graph: IObjectGraph,
136140
val nodeIdTypeConfig: IDataTypeConfiguration<NodeId>,
137141
val treeId: TreeId,
138-
val useRoleIds: Boolean,
139142
) : IObjectDeserializer<NodeObjectData<NodeId>> {
140143
val referenceTypeConfig = LegacyNodeReferenceDataTypeConfig(treeId)
141144
val largeListConfig = LargeListConfig(graph, nodeIdTypeConfig)
@@ -173,11 +176,10 @@ data class NodeObjectData<NodeId>(
173176
deserializer = this@Deserializer,
174177
id = value.id,
175178
concept = value.concept,
176-
containment = decodeNullId(value.parent)?.let { it to IChildLinkReference.fromString(value.role) },
179+
containment = decodeNullId(value.parent)?.let { it to value.role },
177180
children = value.children,
178181
properties = value.properties.toList(),
179182
references = value.references.toList(),
180-
useRoleIds = useRoleIds,
181183
)
182184
}
183185
}

model-datastructure/src/commonMain/kotlin/org/modelix/model/lazy/CLTree.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,17 @@ private fun createNewTreeData(
3737
useRoleIds: Boolean = true,
3838
): Object<CPTree> {
3939
val root = NodeObjectData<Long>(
40-
deserializer = NodeObjectData.Deserializer(graph, LongDataTypeConfiguration(), treeId, useRoleIds = useRoleIds),
40+
deserializer = NodeObjectData.Deserializer(graph, LongDataTypeConfiguration(), treeId),
4141
id = ITree.ROOT_ID,
4242
concept = null,
4343
containment = null,
44-
useRoleIds = useRoleIds,
4544
)
4645
val config = HamtNode.Config(
4746
graph = graph,
4847
keyConfig = LongDataTypeConfiguration(),
4948
valueConfig = ObjectReferenceDataTypeConfiguration(
5049
graph,
51-
NodeObjectData.Deserializer(graph, LongDataTypeConfiguration(), treeId, useRoleIds = useRoleIds),
50+
NodeObjectData.Deserializer(graph, LongDataTypeConfiguration(), treeId),
5251
),
5352
)
5453
@OptIn(DelicateModelixApi::class) // this is a new object

0 commit comments

Comments
 (0)