Skip to content

Commit 9713c48

Browse files
committed
fix(bulk-model-sync): update previously unresolvable references if they can be resolved
For unresolvable references we store the serialized expected reference, as we assume the node is not part of the model but the reference might still be useful for dynamic resolution on access. If the target node became part of the model later on, we did not update it because the originalIds still matched. Now we check, if it was previously unresolvable and update the reference to the INode accordingly.
1 parent 429dcef commit 9713c48

File tree

2 files changed

+89
-10
lines changed

2 files changed

+89
-10
lines changed

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

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,15 @@ class ModelImporter(
8888

8989
private fun PostponedReference.setPostponedReference() {
9090
val expectedRefTarget = originalIdToExisting[expectedTargetId]
91-
if (expectedRefTarget == null) {
91+
if (expectedRefTarget != null) {
92+
node.setReferenceTarget(role, expectedRefTarget)
93+
return
94+
}
95+
// Ensure unresolvable reference wasn't stored before
96+
if (node.getReferenceTargetRef(role)?.serialize() != expectedTargetId) {
9297
// The target node is not part of the model. Assuming it exists in some other model we can
9398
// store the reference and try to resolve it dynamically on access.
9499
node.setReferenceTarget(role, SerializedNodeReference(expectedTargetId))
95-
} else {
96-
node.setReferenceTarget(role, expectedRefTarget)
97100
}
98101
}
99102

@@ -293,16 +296,13 @@ class ModelImporter(
293296
}
294297

295298
private fun syncReferences(node: INode, nodeData: NodeData) {
296-
nodeData.references.forEach {
297-
val expectedTargetId = it.value
298-
val actualTargetId = node.getReferenceTarget(it.key)?.originalId()
299-
?: node.getReferenceTargetRef(it.key)?.serialize()
300-
if (actualTargetId != expectedTargetId) {
299+
nodeData.references.forEach { (referenceRole, expectedTargetId) ->
300+
if (referenceNeedsUpdate(node, referenceRole, expectedTargetId)) {
301301
val expectedTarget = originalIdToExisting[expectedTargetId]
302302
if (expectedTarget == null) {
303-
postponedReferences += PostponedReference(expectedTargetId, node, it.key)
303+
postponedReferences += PostponedReference(expectedTargetId, node, referenceRole)
304304
} else {
305-
node.setReferenceTarget(it.key, expectedTarget)
305+
node.setReferenceTarget(referenceRole, expectedTarget)
306306
}
307307
}
308308
}
@@ -311,6 +311,14 @@ class ModelImporter(
311311
node.setReferenceTarget(it, null as INodeReference?)
312312
}
313313
}
314+
315+
private fun referenceNeedsUpdate(node: INode, referenceRole: String, expectedTargetRef: String): Boolean {
316+
val actualTarget = node.getReferenceTarget(referenceRole)
317+
?: return true // previously unresolvable reference might become resolvable e.g., when a referenced node was added
318+
val actualTargetRef = actualTarget.originalId() ?: node.getReferenceTargetRef(referenceRole)?.serialize()
319+
320+
return actualTargetRef != expectedTargetRef
321+
}
314322
}
315323

316324
internal fun INode.originalId(): String? {

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import org.modelix.model.api.IBranch
2020
import org.modelix.model.api.IProperty
2121
import org.modelix.model.api.IReferenceLink
2222
import org.modelix.model.api.PBranch
23+
import org.modelix.model.api.PNodeReference
2324
import org.modelix.model.api.getRootNode
2425
import org.modelix.model.client.IdGenerator
2526
import org.modelix.model.data.ModelData
@@ -42,6 +43,7 @@ import kotlin.random.Random
4243
import kotlin.reflect.KClass
4344
import kotlin.test.Test
4445
import kotlin.test.assertEquals
46+
import kotlin.test.assertNotNull
4547
import kotlin.test.assertTrue
4648

4749
class ModelImporterTest {
@@ -198,6 +200,75 @@ class ModelImporterTest {
198200
assertEquals(expectedOperations, branch.getNumOfUsedOperationsByType())
199201
}
200202

203+
@Test
204+
@JsName("can_update_previously_unresolvable_references")
205+
fun `can update previously unresolvable reference`() {
206+
// language=json
207+
val initialData = """
208+
{
209+
"root": {
210+
"id": "root",
211+
"children": [
212+
{
213+
"id": "node:001",
214+
"references": {
215+
"myRef": "node:002",
216+
"myRef2": "node:unresolvable"
217+
}
218+
}
219+
]
220+
}
221+
}
222+
""".trimIndent().let { ModelData.fromJson(it) }
223+
224+
// language=json
225+
val expectedData = """
226+
{
227+
"root": {
228+
"id": "root",
229+
"children": [
230+
{
231+
"id": "node:001",
232+
"references": {
233+
"myRef": "node:002",
234+
"myRef2": "node:unresolvable"
235+
}
236+
},
237+
{
238+
"id": "node:002",
239+
"properties": {
240+
"name": "PreviouslyUnresolvableNode"
241+
}
242+
}
243+
]
244+
}
245+
}
246+
""".trimIndent().let { ModelData.fromJson(it) }
247+
248+
val expectedOperations: Map<KClass<out IOperation>, Int> = mapOf(
249+
SetReferenceOp::class to 1,
250+
AddNewChildOp::class to 1,
251+
SetPropertyOp::class to 2, // name and originalRef of new node
252+
)
253+
254+
val branch = createOTBranchFromModel(initialData)
255+
branch.importIncrementally(expectedData)
256+
257+
branch.runRead {
258+
val root = branch.getRootNode()
259+
val sourceNode = root.allChildren.first()
260+
val referenceLink = IReferenceLink.fromName("myRef")
261+
262+
val targetReference = sourceNode.getReferenceTargetRef(referenceLink)
263+
val targetNode = sourceNode.getReferenceTarget(referenceLink)
264+
265+
assertNotNull(targetNode)
266+
assertTrue { targetReference is PNodeReference }
267+
assertAllNodesConformToSpec(expectedData.root, root)
268+
assertEquals(expectedOperations, branch.getNumOfUsedOperationsByType())
269+
}
270+
}
271+
201272
@Test
202273
@JsName("can_sync_children")
203274
fun `can sync children`() {

0 commit comments

Comments
 (0)