Skip to content

Commit 861ac19

Browse files
author
Oleksandr Dzhychko
committed
feat(model-client): add migration for data written with MetaModelBranch
1 parent 9b7ab38 commit 861ac19

File tree

4 files changed

+1071
-6
lines changed

4 files changed

+1071
-6
lines changed

model-api/src/commonMain/kotlin/org/modelix/model/data/ModelData.kt

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,27 @@ data class ModelData(
1818
fun toJson(): String = prettyJson.encodeToString(this)
1919
fun toCompactJson(): String = Json.encodeToString(this)
2020

21-
fun load(branch: IBranch) {
21+
/**
22+
* The [idStrategy] can be provided if nodes should be created with a specific ID.
23+
* [setOriginalIdProperty] whether to safe the original ID in [NodeData.ID_PROPERTY_KEY].
24+
*/
25+
fun load(
26+
branch: IBranch,
27+
idStrategy: ((NodeData) -> Long)? = null,
28+
setOriginalIdProperty: Boolean = true,
29+
) {
2230
branch.computeWriteT { t ->
2331
val createdNodes = HashMap<String, Long>()
2432
val pendingReferences = ArrayList<() -> Unit>()
2533
val parentId = ITree.ROOT_ID
2634
if (root.id != null) {
2735
createdNodes[root.id] = parentId
2836
}
29-
setOriginalId(root, t, parentId)
37+
if (setOriginalIdProperty) {
38+
setOriginalId(root, t, parentId)
39+
}
3040
for (nodeData in root.children) {
31-
loadNode(nodeData, t, parentId, createdNodes, pendingReferences)
41+
loadNode(nodeData, t, parentId, createdNodes, pendingReferences, idStrategy, setOriginalIdProperty)
3242
}
3343
pendingReferences.forEach { it() }
3444
}
@@ -40,12 +50,22 @@ data class ModelData(
4050
parentId: Long,
4151
createdNodes: HashMap<String, Long>,
4252
pendingReferences: ArrayList<() -> Unit>,
53+
idStrategy: ((NodeData) -> Long)?,
54+
setOriginalIdProperty: Boolean,
4355
) {
4456
val conceptRef = nodeData.concept?.let { ConceptReference(it) }
45-
val createdId = t.addNewChild(parentId, nodeData.role, -1, conceptRef)
57+
val createdId = if (idStrategy == null) {
58+
t.addNewChild(parentId, nodeData.role, -1, conceptRef)
59+
} else {
60+
val id = idStrategy(nodeData)
61+
t.addNewChild(parentId, nodeData.role, -1, id, conceptRef)
62+
id
63+
}
4664
if (nodeData.id != null) {
4765
createdNodes[nodeData.id] = createdId
48-
setOriginalId(nodeData, t, createdId)
66+
if (setOriginalIdProperty) {
67+
setOriginalId(nodeData, t, createdId)
68+
}
4969
}
5070
for (propertyData in nodeData.properties) {
5171
t.setProperty(createdId, propertyData.key, propertyData.value)
@@ -57,7 +77,7 @@ data class ModelData(
5777
}
5878
}
5979
for (childData in nodeData.children) {
60-
loadNode(childData, t, createdId, createdNodes, pendingReferences)
80+
loadNode(childData, t, createdId, createdNodes, pendingReferences, idStrategy, setOriginalIdProperty)
6181
}
6282
}
6383

model-client/src/commonMain/kotlin/org/modelix/model/ModelMigrations.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import org.modelix.model.area.PArea
2121
import org.modelix.model.lazy.CLTree
2222
import org.modelix.model.lazy.PrefetchCache
2323
import org.modelix.model.lazy.unwrap
24+
import org.modelix.model.metameta.MetaModelMigration
2425

2526
object ModelMigrations {
2627

@@ -46,4 +47,18 @@ object ModelMigrations {
4647
useCanonicalReferences(t, area, child)
4748
}
4849
}
50+
51+
/**
52+
* Migrates data created with a metamodel to data
53+
* that is readable without the metamodel.
54+
*
55+
* The migration is skipped if no metamodel information exists.
56+
* In a migration, the concept references of all nodes that relied on metamodel information to be resolved
57+
* are changed to concept references that do not need the metamodel.
58+
* In a migration, nodes are recreated with the same ID.
59+
* After the migration, the metamodel is kept for future reference.
60+
*/
61+
fun useResolvedConceptsFromMetaModel(rawBranch: IBranch) {
62+
MetaModelMigration.useResolvedConceptsFromMetaModel(rawBranch)
63+
}
4964
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package org.modelix.model.metameta
15+
16+
import org.modelix.model.api.ConceptReference
17+
import org.modelix.model.api.IBranch
18+
import org.modelix.model.api.IConceptReference
19+
import org.modelix.model.api.ITree
20+
import org.modelix.model.api.IWriteTransaction
21+
import org.modelix.model.api.getRootNode
22+
import org.modelix.model.api.key
23+
import org.modelix.model.persistent.SerializationUtil
24+
25+
internal object MetaModelMigration {
26+
27+
private val HEX_LONG_PATTERN = Regex("[a-fA-Z0-9]+")
28+
29+
private val LOG = mu.KotlinLogging.logger {}
30+
31+
private data class ConceptInformation(
32+
val nodeId: Long,
33+
val originalConcept: IConceptReference?,
34+
val newConcept: IConceptReference?,
35+
)
36+
37+
fun useResolvedConceptsFromMetaModel(rawBranch: IBranch) {
38+
require(rawBranch !is MetaModelBranch) {
39+
"Pass the underlying branch and not the `MetaModelBranch` for migration."
40+
}
41+
LOG.info { "Start migration for `$rawBranch`." }
42+
rawBranch.runWriteT { transaction ->
43+
val root = rawBranch.getRootNode()
44+
val languageNodes = root.getChildren(MetaModelIndex.LANGUAGES_ROLE).toList()
45+
val needsMigration = languageNodes.isNotEmpty()
46+
if (!needsMigration) {
47+
LOG.info { "Migration not needed for `$rawBranch` because no meta model information is stored." }
48+
return@runWriteT
49+
}
50+
val originalData = transaction.tree
51+
migrateChildren(transaction, originalData, ITree.ROOT_ID)
52+
}
53+
LOG.info { "Finish migration for `$rawBranch`." }
54+
}
55+
56+
private fun migrateChildren(newData: IWriteTransaction, originalData: ITree, parentId: Long) {
57+
val childIds = newData.getAllChildren(parentId)
58+
59+
val conceptInformationForChildren = childIds.map { childId ->
60+
val originalConcept = newData.getConceptReference(childId)
61+
val newConcept = resolveNewConceptReference(originalConcept, originalData)
62+
ConceptInformation(childId, originalConcept, newConcept)
63+
}
64+
65+
val noConceptChanged = conceptInformationForChildren.all { it.newConcept == it.originalConcept }
66+
67+
if (noConceptChanged) {
68+
childIds.forEach { childId ->
69+
migrateChildren(newData, originalData, childId)
70+
}
71+
} else {
72+
conceptInformationForChildren.forEach { conceptInformationForChild ->
73+
val childId = conceptInformationForChild.nodeId
74+
val originalConcept = conceptInformationForChild.originalConcept
75+
val newConcept = conceptInformationForChild.newConcept
76+
val keepConcept = originalConcept?.getUID() == newConcept?.getUID()
77+
78+
if (keepConcept) {
79+
migrateChildren(newData, originalData, childId)
80+
newData.moveChild(parentId, originalData.getRole(childId), -1, childId)
81+
} else {
82+
// Currently we have no mechanism to efficiently replace the concept on ITransaction.
83+
// This will become available with https://issues.modelix.org/issue/MODELIX-710
84+
// For this migration, we can just recreate the children with the same IDs.
85+
newData.deleteNode(childId)
86+
newData.addNewChild(parentId, originalData.getRole(childId), -1, childId, newConcept)
87+
createChildren(newData, originalData, childId)
88+
}
89+
}
90+
}
91+
}
92+
93+
private fun createChildren(newData: IWriteTransaction, originalData: ITree, parentId: Long) {
94+
writeProperties(newData, originalData, parentId)
95+
writeReferences(newData, originalData, parentId)
96+
for (childId in originalData.getAllChildren(parentId)) {
97+
val originalConcept = originalData.getConceptReference(childId)
98+
val newConcept = resolveNewConceptReference(originalConcept, originalData)
99+
newData.addNewChild(parentId, originalData.getRole(childId), -1, childId, newConcept)
100+
createChildren(newData, originalData, childId)
101+
}
102+
}
103+
104+
private fun writeReferences(newData: IWriteTransaction, originalData: ITree, parentId: Long) {
105+
val referenceRoles = originalData.getReferenceRoles(parentId)
106+
for (referenceRole in referenceRoles) {
107+
val referenceValue = originalData.getReferenceTarget(parentId, referenceRole)
108+
newData.setReferenceTarget(parentId, referenceRole, referenceValue)
109+
}
110+
}
111+
112+
private fun writeProperties(
113+
t: IWriteTransaction,
114+
originalData: ITree,
115+
parentId: Long,
116+
) {
117+
val propertyRoles = originalData.getPropertyRoles(parentId)
118+
for (propertyRole in propertyRoles) {
119+
val propertyValue = originalData.getProperty(parentId, propertyRole)
120+
t.setProperty(parentId, propertyRole, propertyValue)
121+
}
122+
}
123+
124+
fun resolveNewConceptReference(originalConceptRef: IConceptReference?, tree: ITree): IConceptReference? {
125+
if (originalConceptRef == null) {
126+
return originalConceptRef
127+
}
128+
129+
val originalUID = originalConceptRef.getUID()
130+
if (!(originalUID matches HEX_LONG_PATTERN)) {
131+
return originalConceptRef
132+
}
133+
134+
val conceptNodeId = SerializationUtil.longFromHex(originalUID)
135+
if (!tree.containsNode(conceptNodeId)) {
136+
return originalConceptRef
137+
}
138+
val uid = tree.getProperty(conceptNodeId, MetaMetaLanguage.property_IHasUID_uid.key(tree))
139+
?: return originalConceptRef
140+
141+
return ConceptReference(uid)
142+
}
143+
}

0 commit comments

Comments
 (0)