Skip to content

Commit f03e5f5

Browse files
authored
Merge pull request #636 from modelix/feature/bulk-sync-handle-concept-changes
MODELIX-710 bulk-model-sync-gradle fails with "unexpected concept change"
2 parents 0ed9624 + af2af95 commit f03e5f5

File tree

42 files changed

+546
-35
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

+546
-35
lines changed

.github/workflows/mps-compatibility.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ jobs:
1616
strategy:
1717
matrix:
1818
version:
19-
- "2020.3"
2019
- "2021.1"
2120
- "2021.2"
2221
- "2021.3"

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

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import org.modelix.model.api.ConceptReference
2222
import org.modelix.model.api.INode
2323
import org.modelix.model.api.INodeReference
2424
import org.modelix.model.api.INodeResolutionScope
25+
import org.modelix.model.api.IReplaceableNode
2526
import org.modelix.model.api.SerializedNodeReference
2627
import org.modelix.model.api.getDescendants
2728
import org.modelix.model.api.isChildRoleOrdered
@@ -162,9 +163,10 @@ class ModelImporter(
162163
currentNodeProgress += 1
163164
progressReporter.step(currentNodeProgress.toULong())
164165
doAndPotentiallyContinueOnErrors {
165-
syncProperties(node, data)
166-
syncChildren(node, data, progressReporter)
167-
syncReferences(node, data)
166+
val conceptCorrectedNode = checkAndHandleConceptChange(node, data)
167+
syncProperties(conceptCorrectedNode, data)
168+
syncChildren(conceptCorrectedNode, data, progressReporter)
169+
syncReferences(conceptCorrectedNode, data)
168170
}
169171
}
170172

@@ -200,7 +202,9 @@ class ModelImporter(
200202
// optimization for when there is no change in the child list
201203
// size check first to avoid querying the original ID
202204
if (expectedNodes.size == existingNodes.size && expectedNodes.map { it.originalId() } == existingNodes.map { it.originalId() }) {
203-
existingNodes.zip(expectedNodes).forEach { syncNode(it.first, it.second, progressReporter) }
205+
existingNodes.zip(expectedNodes).forEach {
206+
syncNode(it.first, it.second, progressReporter)
207+
}
204208
continue
205209
}
206210

@@ -255,8 +259,6 @@ class ModelImporter(
255259
} else {
256260
nodeAtIndex
257261
}
258-
check(childNode.getConceptReference() == expectedConcept) { "Unexpected concept change from $expectedConcept to ${childNode.getConceptReference()}" }
259-
260262
syncNode(childNode, expected, progressReporter)
261263
}
262264

@@ -270,6 +272,15 @@ class ModelImporter(
270272
}
271273
}
272274

275+
private fun checkAndHandleConceptChange(existingNode: INode, expectedNode: NodeData): INode {
276+
val newConcept = expectedNode.concept?.let { ConceptReference(it) }
277+
if (existingNode.getConceptReference() == newConcept) {
278+
return existingNode
279+
}
280+
require(existingNode is IReplaceableNode) { "Concept has changed but node is not replaceable. node=$existingNode" }
281+
return existingNode.replaceNode(newConcept)
282+
}
283+
273284
private fun buildExistingIndex(): MutableMap<String, INodeReference> {
274285
val localOriginalIdToExisting = createMemoryEfficientMap<String, INodeReference>()
275286
root.getDescendants(true).forEach { node ->

bulk-model-sync-lib/src/commonTest/kotlin/org/modelix/model/sync/bulk/ModelImporterTest.kt

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,76 @@ class ModelImporterTest {
399399
assertEquals(expectedOperations, branch.getNumOfUsedOperationsByType())
400400
}
401401

402+
@Test
403+
@JsName("can_handle_concept_changes")
404+
fun `can handle concept changes`() {
405+
// language=json
406+
val initialData = """
407+
{
408+
"root": {
409+
"id": "node:001",
410+
"children": [
411+
{
412+
"id": "node:002",
413+
"concept": "ref:MyOldConcept",
414+
"children": [
415+
{
416+
"id": "child02",
417+
"references": {
418+
"myReference": "node:003"
419+
},
420+
"properties": {
421+
"myProperty": "myPropertyValue"
422+
}
423+
}
424+
]
425+
},
426+
{
427+
"id": "node:003"
428+
}
429+
]
430+
}
431+
}
432+
""".trimIndent().let { ModelData.fromJson(it) }
433+
434+
// language=json
435+
val expectedData = """
436+
{
437+
"root": {
438+
"id": "node:001",
439+
"children": [
440+
{
441+
"id": "node:002",
442+
"concept": "ref:MyNewConcept",
443+
"children": [
444+
{
445+
"id": "child02",
446+
"references": {
447+
"myReference": "node:003"
448+
},
449+
"properties": {
450+
"myProperty": "myPropertyValue"
451+
}
452+
}
453+
]
454+
},
455+
{
456+
"id": "node:003"
457+
}
458+
]
459+
}
460+
}
461+
""".trimIndent().let { ModelData.fromJson(it) }
462+
463+
val branch = createOTBranchFromModel(initialData)
464+
branch.importIncrementally(expectedData)
465+
466+
branch.runRead {
467+
val changedNode = branch.getRootNode().allChildren.first()
468+
assertEquals("ref:MyNewConcept", changedNode.getConceptReference().toString())
469+
}
470+
}
471+
402472
@Test
403473
@JsName("can_import_complex_model")
404474
fun `can import complex model`() {

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,20 @@ interface IDeprecatedNodeDefaults : INode {
297297
override fun getReferenceLinks(): List<IReferenceLink>
298298
}
299299

300+
/**
301+
* Interface for nodes that can be replaced by a new instance.
302+
*/
303+
interface IReplaceableNode : INode {
304+
/**
305+
* Replaces this node with a new node of the given concept that uses the same id as this node.
306+
* Properties, references and children will be the same.
307+
*
308+
* @param concept the concept of the new node
309+
* @return replacement for this node with the new given concept
310+
*/
311+
fun replaceNode(concept: ConceptReference?): INode
312+
}
313+
300314
@Deprecated("Use .key(INode), .key(IBranch), .key(ITransaction) or .key(ITree)")
301315
fun IRole.key(): String = RoleAccessContext.getKey(this)
302316
fun IRole.key(node: INode): String = if (node.usesRoleIds()) getUID() else getSimpleName()

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,12 @@ interface ITree {
254254
*/
255255
fun deleteNodes(nodeIds: LongArray): ITree
256256

257+
/**
258+
* @param nodeId id of the node
259+
* @param concept the concept ref of the new concept for the node, or null to remove the concept
260+
*/
261+
fun setConcept(nodeId: Long, concept: IConceptReference?): ITree
262+
257263
fun getAllChildrenAsFlow(parentId: Long): Flow<Long> = getAllChildren(parentId).asFlow()
258264
fun getDescendantsAsFlow(nodeId: Long, includeSelf: Boolean = false): Flow<Long> {
259265
return if (includeSelf) {

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ interface ITreeChangeVisitor {
2626
*/
2727
fun containmentChanged(nodeId: Long)
2828

29+
/**
30+
* Called when the concept of a node has changed.
31+
*
32+
* @param nodeId id of the affected node
33+
*/
34+
fun conceptChanged(nodeId: Long) {} // Noop default implementation to avoid breaking change
35+
2936
/**
3037
* Called when the children of a node have changed.
3138
*

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ interface IWriteTransaction : ITransaction {
105105
fun addNewChildren(parentId: Long, role: String?, index: Int, concepts: Array<IConceptReference?>): LongArray
106106
fun addNewChildren(parentId: Long, role: String?, index: Int, childIds: LongArray, concepts: Array<IConceptReference?>)
107107

108+
/**
109+
* Sets the concept of the node identified by the given id.
110+
*
111+
* @param nodeId id of the node
112+
* @param concept the new concept, or null to remove the concept
113+
*/
114+
fun setConcept(nodeId: Long, concept: IConceptReference?)
115+
108116
/**
109117
* Deletes the given node.
110118
*

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.map
2020
import kotlinx.coroutines.flow.mapNotNull
2121
import org.modelix.model.area.PArea
2222

23-
open class PNodeAdapter(val nodeId: Long, val branch: IBranch) : INode, INodeEx {
23+
open class PNodeAdapter(val nodeId: Long, val branch: IBranch) : INode, INodeEx, IReplaceableNode {
2424

2525
init {
2626
require(nodeId != 0L, { "Invalid node 0" })
@@ -82,6 +82,11 @@ open class PNodeAdapter(val nodeId: Long, val branch: IBranch) : INode, INodeEx
8282
return PNodeAdapter(branch.writeTransaction.addNewChild(nodeId, role.key(this), index, concept), branch)
8383
}
8484

85+
override fun replaceNode(concept: ConceptReference?): INode {
86+
branch.writeTransaction.setConcept(nodeId, concept)
87+
return this
88+
}
89+
8590
override val allChildren: Iterable<INode>
8691
get() {
8792
notifyAccess()

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ class TreePointer(private var tree_: ITree, val idGenerator: IIdGenerator = IdGe
138138
tree = tree.addNewChildren(parentId, role, index, childIds, concepts)
139139
}
140140

141+
override fun setConcept(nodeId: Long, concept: IConceptReference?) {
142+
tree = tree.setConcept(nodeId, concept)
143+
}
144+
141145
override fun deleteNode(nodeId: Long) {
142146
tree = tree.deleteNode(nodeId)
143147
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ class WriteTransaction(_tree: ITree, branch: IBranch, idGenerator: IIdGenerator)
9191
return newIds
9292
}
9393

94+
override fun setConcept(nodeId: Long, concept: IConceptReference?) {
95+
checkNotClosed()
96+
tree = tree.setConcept(nodeId, concept)
97+
}
98+
9499
override fun deleteNode(nodeId: Long) {
95100
checkNotClosed()
96101
tree.getAllChildren(nodeId).forEach { deleteNode(it) }

0 commit comments

Comments
 (0)