infrahub-server 1.2.9__py3-none-any.whl → 1.2.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. infrahub/computed_attribute/models.py +13 -0
  2. infrahub/computed_attribute/tasks.py +29 -28
  3. infrahub/core/attribute.py +43 -2
  4. infrahub/core/branch/models.py +8 -9
  5. infrahub/core/diff/calculator.py +61 -8
  6. infrahub/core/diff/combiner.py +37 -29
  7. infrahub/core/diff/enricher/hierarchy.py +4 -6
  8. infrahub/core/diff/merger/merger.py +29 -1
  9. infrahub/core/diff/merger/serializer.py +1 -0
  10. infrahub/core/diff/model/path.py +6 -3
  11. infrahub/core/diff/query/merge.py +264 -28
  12. infrahub/core/diff/query/save.py +6 -5
  13. infrahub/core/diff/query_parser.py +4 -15
  14. infrahub/core/diff/repository/deserializer.py +7 -6
  15. infrahub/core/graph/__init__.py +1 -1
  16. infrahub/core/migrations/graph/m028_delete_diffs.py +38 -0
  17. infrahub/core/query/diff.py +97 -13
  18. infrahub/core/query/node.py +26 -3
  19. infrahub/core/query/relationship.py +96 -35
  20. infrahub/core/relationship/model.py +1 -1
  21. infrahub/core/validators/uniqueness/query.py +7 -0
  22. infrahub/trigger/setup.py +13 -2
  23. infrahub/types.py +1 -1
  24. infrahub/webhook/models.py +2 -1
  25. infrahub/workflows/catalogue.py +9 -0
  26. infrahub_sdk/timestamp.py +2 -2
  27. {infrahub_server-1.2.9.dist-info → infrahub_server-1.2.10.dist-info}/METADATA +3 -3
  28. {infrahub_server-1.2.9.dist-info → infrahub_server-1.2.10.dist-info}/RECORD +33 -32
  29. infrahub_testcontainers/docker-compose.test.yml +2 -2
  30. infrahub_testcontainers/performance_test.py +6 -3
  31. {infrahub_server-1.2.9.dist-info → infrahub_server-1.2.10.dist-info}/LICENSE.txt +0 -0
  32. {infrahub_server-1.2.9.dist-info → infrahub_server-1.2.10.dist-info}/WHEEL +0 -0
  33. {infrahub_server-1.2.9.dist-info → infrahub_server-1.2.10.dist-info}/entry_points.txt +0 -0
@@ -87,13 +87,13 @@ class NodeIdentifier:
87
87
 
88
88
  uuid: str
89
89
  kind: str
90
- labels: frozenset[str]
90
+ db_id: str
91
91
 
92
92
  def __hash__(self) -> int:
93
- return hash(f"{self.uuid}:{self.kind}:{hash(self.labels)}")
93
+ return hash(f"{self.uuid}:{self.kind}:{self.db_id}")
94
94
 
95
95
  def __str__(self) -> str:
96
- return f"{self.kind} '{self.uuid}' ({','.join(self.labels)})"
96
+ return f"{self.kind} '{self.uuid}' ({self.db_id})"
97
97
 
98
98
 
99
99
  @dataclass
@@ -327,6 +327,7 @@ class EnrichedDiffNode(BaseSummary):
327
327
  path_identifier: str = field(default="", kw_only=True)
328
328
  changed_at: Timestamp | None = field(default=None, kw_only=True)
329
329
  action: DiffAction
330
+ is_node_kind_migration: bool = field(default=False)
330
331
  conflict: EnrichedDiffConflict | None = field(default=None)
331
332
  attributes: set[EnrichedDiffAttribute] = field(default_factory=set)
332
333
  relationships: set[EnrichedDiffRelationship] = field(default_factory=set)
@@ -428,6 +429,7 @@ class EnrichedDiffNode(BaseSummary):
428
429
  label="",
429
430
  changed_at=calculated_node.changed_at,
430
431
  action=calculated_node.action,
432
+ is_node_kind_migration=calculated_node.is_node_kind_migration,
431
433
  attributes={
432
434
  EnrichedDiffAttribute.from_calculated_attribute(calculated_attribute=attr)
433
435
  for attr in calculated_node.attributes
@@ -686,6 +688,7 @@ class DiffNode:
686
688
  identifier: NodeIdentifier
687
689
  changed_at: Timestamp
688
690
  action: DiffAction
691
+ is_node_kind_migration: bool = field(default=False)
689
692
  attributes: list[DiffAttribute] = field(default_factory=list)
690
693
  relationships: list[DiffRelationship] = field(default_factory=list)
691
694
 
@@ -20,6 +20,7 @@ class DiffMergeQuery(Query):
20
20
  node_diff_dicts: dict[str, Any],
21
21
  at: Timestamp,
22
22
  target_branch: Branch,
23
+ migrated_kinds_id_map: dict[str, str],
23
24
  **kwargs: Any,
24
25
  ) -> None:
25
26
  super().__init__(**kwargs)
@@ -27,6 +28,7 @@ class DiffMergeQuery(Query):
27
28
  self.at = at
28
29
  self.target_branch = target_branch
29
30
  self.source_branch_name = self.branch.name
31
+ self.migrated_kinds_id_map = migrated_kinds_id_map
30
32
 
31
33
  async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
32
34
  self.params = {
@@ -35,19 +37,29 @@ class DiffMergeQuery(Query):
35
37
  "branch_level": self.target_branch.hierarchy_level,
36
38
  "target_branch": self.target_branch.name,
37
39
  "source_branch": self.source_branch_name,
40
+ "migrated_kinds_id_map": self.migrated_kinds_id_map,
41
+ "migrated_kinds_uuids": list(self.migrated_kinds_id_map.keys()),
38
42
  }
39
43
  # ruff: noqa: E501
40
44
  query = """
41
45
  UNWIND $node_diff_dicts AS node_diff_map
46
+ WITH node_diff_map, node_diff_map.uuid IN $migrated_kinds_uuids AS is_node_kind_migration
47
+ WITH node_diff_map, is_node_kind_migration, CASE
48
+ WHEN $migrated_kinds_uuids IS NULL THEN NULL
49
+ WHEN is_node_kind_migration THEN $migrated_kinds_id_map[node_diff_map.uuid]
50
+ ELSE NULL
51
+ END AS node_db_id
42
52
  CALL {
43
- WITH node_diff_map
53
+ WITH node_diff_map, node_db_id
44
54
  MATCH (n:Node {uuid: node_diff_map.uuid})
55
+ WHERE node_db_id IS NULL
56
+ OR %(id_func)s(n) = node_db_id
45
57
  RETURN n
46
58
  }
47
- WITH n, node_diff_map
59
+ WITH n, node_diff_map, is_node_kind_migration
48
60
  CALL {
49
- WITH n, node_diff_map
50
- WITH n, node_diff_map, CASE
61
+ WITH n, node_diff_map, is_node_kind_migration
62
+ WITH n, node_diff_map, is_node_kind_migration, CASE
51
63
  WHEN node_diff_map.action = "ADDED" THEN "active"
52
64
  WHEN node_diff_map.action = "REMOVED" THEN "deleted"
53
65
  ELSE NULL
@@ -56,9 +68,11 @@ CALL {
56
68
  // ------------------------------
57
69
  // only make IS_PART_OF updates if node is ADDED or REMOVED
58
70
  // ------------------------------
59
- WITH n, node_diff_map, node_rel_status
60
- WITH n, node_diff_map, node_rel_status
71
+ WITH n, node_diff_map, node_rel_status, is_node_kind_migration
72
+ WITH n, node_diff_map, node_rel_status, is_node_kind_migration
61
73
  WHERE node_rel_status IS NOT NULL
74
+ // nodes with a migrated kind are handled in DiffMergeMigratedKindsQuery
75
+ AND is_node_kind_migration = FALSE
62
76
  MATCH (root:Root)
63
77
  // ------------------------------
64
78
  // set IS_PART_OF.to, optionally, target branch
@@ -214,22 +228,30 @@ CALL {
214
228
  // ------------------------------
215
229
  CALL {
216
230
  WITH n, relationship_diff_map
217
- WITH n, relationship_diff_map.peer_id AS rel_peer_id, relationship_diff_map.name AS rel_name, CASE
218
- WHEN relationship_diff_map.action = "ADDED" THEN "active"
219
- WHEN relationship_diff_map.action = "REMOVED" THEN "deleted"
220
- ELSE NULL
221
- END AS related_rel_status
231
+ WITH
232
+ n, relationship_diff_map.peer_id AS rel_peer_id, relationship_diff_map.name AS rel_name,
233
+ CASE
234
+ WHEN relationship_diff_map.action = "ADDED" THEN "active"
235
+ WHEN relationship_diff_map.action = "REMOVED" THEN "deleted"
236
+ ELSE NULL
237
+ END AS related_rel_status,
238
+ CASE
239
+ WHEN $migrated_kinds_uuids IS NULL THEN NULL
240
+ WHEN relationship_diff_map.peer_id IN $migrated_kinds_uuids THEN $migrated_kinds_id_map[relationship_diff_map.peer_id]
241
+ ELSE NULL
242
+ END AS rel_peer_db_id
222
243
  // ------------------------------
223
244
  // determine the directions of each IS_RELATED
224
245
  // ------------------------------
225
246
  CALL {
226
- WITH n, rel_name, rel_peer_id, related_rel_status
247
+ WITH n, rel_name, rel_peer_id, rel_peer_db_id, related_rel_status
227
248
  MATCH (n)
228
249
  -[source_r_rel_1:IS_RELATED]
229
250
  -(r:Relationship {name: rel_name})
230
251
  -[source_r_rel_2:IS_RELATED]
231
- -(:Node {uuid: rel_peer_id})
232
- WHERE source_r_rel_1.branch IN [$source_branch, $target_branch]
252
+ -(rel_peer:Node {uuid: rel_peer_id})
253
+ WHERE (rel_peer_db_id IS NULL OR %(id_func)s(rel_peer) = rel_peer_db_id)
254
+ AND source_r_rel_1.branch IN [$source_branch, $target_branch]
233
255
  AND source_r_rel_2.branch IN [$source_branch, $target_branch]
234
256
  AND source_r_rel_1.from <= $at AND source_r_rel_1.to IS NULL
235
257
  AND source_r_rel_2.from <= $at AND source_r_rel_2.to IS NULL
@@ -247,27 +269,29 @@ CALL {
247
269
  source_r_rel_1.hierarchy AS r1_hierarchy,
248
270
  source_r_rel_2.hierarchy AS r2_hierarchy
249
271
  }
250
- WITH n, r, r1_dir, r2_dir, r1_hierarchy, r2_hierarchy, rel_name, rel_peer_id, related_rel_status
272
+ WITH n, r, r1_dir, r2_dir, r1_hierarchy, r2_hierarchy, rel_name, rel_peer_id, rel_peer_db_id, related_rel_status
251
273
  CALL {
252
- WITH n, rel_name, rel_peer_id, related_rel_status
274
+ WITH n, rel_name, rel_peer_id, rel_peer_db_id, related_rel_status
253
275
  OPTIONAL MATCH (n)
254
276
  -[target_r_rel_1:IS_RELATED {branch: $target_branch, status: "active"}]
255
277
  -(:Relationship {name: rel_name})
256
278
  -[target_r_rel_2:IS_RELATED {branch: $target_branch, status: "active"}]
257
- -(:Node {uuid: rel_peer_id})
279
+ -(rel_peer:Node {uuid: rel_peer_id})
258
280
  WHERE related_rel_status = "deleted"
281
+ AND (rel_peer_db_id IS NULL OR %(id_func)s(rel_peer) = rel_peer_db_id)
259
282
  AND target_r_rel_1.from <= $at AND target_r_rel_1.to IS NULL
260
283
  AND target_r_rel_2.from <= $at AND target_r_rel_2.to IS NULL
261
284
  SET target_r_rel_1.to = $at
262
285
  SET target_r_rel_2.to = $at
263
286
  }
264
- WITH n, r, r1_dir, r2_dir, r1_hierarchy, r2_hierarchy, rel_name, rel_peer_id, related_rel_status
287
+ WITH n, r, r1_dir, r2_dir, r1_hierarchy, r2_hierarchy, rel_name, rel_peer_id, rel_peer_db_id, related_rel_status
265
288
  // ------------------------------
266
289
  // conditionally create new IS_RELATED relationships on target_branch, if necessary
267
290
  // ------------------------------
268
291
  CALL {
269
- WITH n, r, r1_dir, r2_dir, r1_hierarchy, r2_hierarchy, rel_name, rel_peer_id, related_rel_status
292
+ WITH n, r, r1_dir, r2_dir, r1_hierarchy, r2_hierarchy, rel_name, rel_peer_id, rel_peer_db_id, related_rel_status
270
293
  MATCH (p:Node {uuid: rel_peer_id})
294
+ WHERE rel_peer_db_id IS NULL OR %(id_func)s(p) = rel_peer_db_id
271
295
  OPTIONAL MATCH (n)
272
296
  -[r_rel_1:IS_RELATED {branch: $target_branch, status: related_rel_status}]
273
297
  -(:Relationship {name: rel_name})
@@ -320,7 +344,7 @@ CALL {
320
344
  }
321
345
  }
322
346
  RETURN 1 AS done
323
- """
347
+ """ % {"id_func": db.get_id_function_name()}
324
348
  self.add_to_query(query=query)
325
349
 
326
350
 
@@ -334,6 +358,7 @@ class DiffMergePropertiesQuery(Query):
334
358
  property_diff_dicts: dict[str, Any],
335
359
  at: Timestamp,
336
360
  target_branch: Branch,
361
+ migrated_kinds_id_map: dict[str, str],
337
362
  **kwargs: Any,
338
363
  ) -> None:
339
364
  super().__init__(**kwargs)
@@ -341,6 +366,7 @@ class DiffMergePropertiesQuery(Query):
341
366
  self.at = at
342
367
  self.target_branch = target_branch
343
368
  self.source_branch_name = self.branch.name
369
+ self.migrated_kinds_id_map = migrated_kinds_id_map
344
370
 
345
371
  async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
346
372
  self.params = {
@@ -349,53 +375,70 @@ class DiffMergePropertiesQuery(Query):
349
375
  "branch_level": self.target_branch.hierarchy_level,
350
376
  "target_branch": self.target_branch.name,
351
377
  "source_branch": self.source_branch_name,
378
+ "migrated_kinds_id_map": self.migrated_kinds_id_map,
379
+ "migrated_kinds_uuids": list(self.migrated_kinds_id_map.keys()),
352
380
  }
353
381
  query = """
354
382
  UNWIND $property_diff_dicts AS attr_rel_prop_diff
383
+ WITH attr_rel_prop_diff, CASE
384
+ WHEN $migrated_kinds_uuids IS NULL THEN NULL
385
+ WHEN attr_rel_prop_diff.node_uuid IN $migrated_kinds_uuids THEN $migrated_kinds_id_map[attr_rel_prop_diff.node_uuid]
386
+ ELSE NULL
387
+ END AS node_db_id,
388
+ CASE
389
+ WHEN $migrated_kinds_uuids IS NULL THEN NULL
390
+ WHEN attr_rel_prop_diff.peer_uuid IN $migrated_kinds_uuids THEN $migrated_kinds_id_map[attr_rel_prop_diff.peer_uuid]
391
+ ELSE NULL
392
+ END AS peer_db_id
355
393
  CALL {
356
394
  // ------------------------------
357
395
  // find the Attribute node
358
396
  // ------------------------------
359
- WITH attr_rel_prop_diff
397
+ WITH attr_rel_prop_diff, node_db_id, peer_db_id
360
398
  CALL {
361
- WITH attr_rel_prop_diff
399
+ WITH attr_rel_prop_diff, node_db_id
362
400
  OPTIONAL MATCH (n:Node {uuid: attr_rel_prop_diff.node_uuid})
363
401
  -[has_attr:HAS_ATTRIBUTE]
364
402
  ->(attr:Attribute {name: attr_rel_prop_diff.attribute_name})
365
403
  WHERE attr_rel_prop_diff.attribute_name IS NOT NULL
404
+ AND (node_db_id IS NULL OR %(id_func)s(n) = node_db_id)
366
405
  AND has_attr.branch IN [$source_branch, $target_branch]
367
406
  RETURN attr
368
407
  ORDER BY has_attr.from DESC
369
408
  LIMIT 1
370
409
  }
371
410
  CALL {
372
- WITH attr_rel_prop_diff
411
+ WITH attr_rel_prop_diff, node_db_id, peer_db_id
373
412
  OPTIONAL MATCH (n:Node {uuid: attr_rel_prop_diff.node_uuid})
374
413
  -[r1:IS_RELATED]
375
414
  -(rel:Relationship {name: attr_rel_prop_diff.relationship_id})
376
415
  -[r2:IS_RELATED]
377
- -(:Node {uuid: attr_rel_prop_diff.peer_uuid})
416
+ -(rel_peer:Node {uuid: attr_rel_prop_diff.peer_uuid})
378
417
  WHERE attr_rel_prop_diff.relationship_id IS NOT NULL
418
+ AND (node_db_id IS NULL OR %(id_func)s(n) = node_db_id)
419
+ AND (peer_db_id IS NULL OR %(id_func)s(rel_peer) = peer_db_id)
379
420
  AND r1.branch IN [$source_branch, $target_branch]
380
421
  AND r2.branch IN [$source_branch, $target_branch]
381
422
  RETURN rel
382
423
  ORDER BY r1.branch_level DESC, r2.branch_level DESC, r1.from DESC, r2.from DESC
383
424
  LIMIT 1
384
425
  }
385
- WITH attr_rel_prop_diff, COALESCE(attr, rel) AS attr_rel
426
+ WITH attr_rel_prop_diff, COALESCE(attr, rel) AS attr_rel, peer_db_id
427
+ WHERE attr_rel IS NOT NULL
386
428
  UNWIND attr_rel_prop_diff.properties AS property_diff
387
429
  // ------------------------------
388
430
  // handle updates for properties under this attribute/relationship
389
431
  // ------------------------------
390
432
  CALL {
391
- WITH attr_rel, property_diff
433
+ WITH attr_rel, property_diff, peer_db_id
392
434
  // ------------------------------
393
435
  // identify the correct property node to link
394
436
  // ------------------------------
395
437
  CALL {
396
- WITH attr_rel, property_diff
438
+ WITH attr_rel, property_diff, peer_db_id
397
439
  OPTIONAL MATCH (peer:Node {uuid: property_diff.value})
398
440
  WHERE property_diff.property_type IN ["HAS_SOURCE", "HAS_OWNER"]
441
+ AND (peer_db_id IS NULL OR %(id_func)s(peer) = peer_db_id)
399
442
  // ------------------------------
400
443
  // the serialized diff might not include the values for IS_VISIBLE and IS_PROTECTED in
401
444
  // some cases, so we need to figure them out here
@@ -497,10 +540,203 @@ CALL {
497
540
  }
498
541
  }
499
542
  }
500
- """
543
+ """ % {"id_func": db.get_id_function_name()}
501
544
  self.add_to_query(query=query)
502
545
 
503
546
 
547
+ class DiffMergeMigratedKindsQuery(Query):
548
+ name = "diff_merge_migrated_kinds"
549
+ type = QueryType.WRITE
550
+ insert_return = False
551
+
552
+ def __init__(
553
+ self,
554
+ migrated_uuids: list[str],
555
+ at: Timestamp,
556
+ target_branch: Branch,
557
+ **kwargs: Any,
558
+ ) -> None:
559
+ super().__init__(**kwargs)
560
+ self.migrated_uuids = migrated_uuids
561
+ self.at = at
562
+ self.target_branch = target_branch
563
+ self.source_branch_name = self.branch.name
564
+
565
+ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
566
+ self.params = {
567
+ "migrated_uuids": self.migrated_uuids,
568
+ "at": self.at.to_string(),
569
+ "branch_level": self.target_branch.hierarchy_level,
570
+ "target_branch": self.target_branch.name,
571
+ "source_branch": self.source_branch_name,
572
+ }
573
+ query = """
574
+ MATCH (n:Node)
575
+ WHERE n.uuid IN $migrated_uuids
576
+ CALL {
577
+ // --------------
578
+ // for each migrated node (created or deleted), find its latest edges on the source branch,
579
+ // check if they exist on the target, create them if not
580
+ // --------------
581
+ WITH n
582
+ MATCH (n)-[]-(peer)
583
+ WITH DISTINCT n, peer
584
+ CALL {
585
+ // --------------
586
+ // get the latest outbound edge for each type between n and peer
587
+ // --------------
588
+ WITH n, peer
589
+ MATCH (n)-[e {branch: $source_branch}]->(peer)
590
+ WHERE e.from <= $at AND e.to IS NULL
591
+ WITH e, type(e) AS edge_type
592
+ ORDER BY edge_type, e.from DESC
593
+ WITH edge_type, head(collect(e)) AS latest_source_edge
594
+ RETURN edge_type, latest_source_edge
595
+ }
596
+ CALL {
597
+ // --------------
598
+ // for each n, peer, edge_type, get the latest edge on target
599
+ // --------------
600
+ WITH n, peer, edge_type
601
+ OPTIONAL MATCH (n)-[e {branch: $target_branch}]->(peer)
602
+ WHERE type(e) = edge_type AND e.from <= $at
603
+ RETURN e AS latest_target_edge
604
+ ORDER BY e.from DESC
605
+ LIMIT 1
606
+ }
607
+ // --------------
608
+ // ignore edges of this type that already have the correct status on the target branch
609
+ // --------------
610
+ WITH n, peer, edge_type, latest_source_edge, latest_target_edge
611
+ WHERE (latest_target_edge IS NULL AND latest_source_edge.status = "active")
612
+ OR latest_source_edge.status <> latest_target_edge.status
613
+ CALL {
614
+ // --------------
615
+ // set the to time on active target branch edges that we are setting to deleted
616
+ // --------------
617
+ WITH latest_source_edge, latest_target_edge
618
+ WITH latest_source_edge, latest_target_edge
619
+ WHERE latest_target_edge IS NOT NULL
620
+ AND latest_source_edge.status = "deleted"
621
+ AND latest_target_edge.status = "active"
622
+ AND latest_target_edge.to IS NULL
623
+ SET latest_target_edge.to = $at
624
+ }
625
+ // --------------
626
+ // create the outbound edges on the target branch, one subquery per possible type
627
+ // --------------
628
+ CALL {
629
+ WITH n, latest_source_edge, peer, edge_type
630
+ WITH n, latest_source_edge, peer, edge_type
631
+ WHERE edge_type = "IS_PART_OF"
632
+ CREATE (n)-[new_edge:IS_PART_OF]->(peer)
633
+ SET new_edge = properties(latest_source_edge)
634
+ SET new_edge.from = $at
635
+ SET new_edge.branch_level = $branch_level
636
+ SET new_edge.branch = $target_branch
637
+ }
638
+ CALL {
639
+ WITH n, latest_source_edge, peer, edge_type
640
+ WITH n, latest_source_edge, peer, edge_type
641
+ WHERE edge_type = "IS_RELATED"
642
+ CREATE (n)-[new_edge:IS_RELATED]->(peer)
643
+ SET new_edge = properties(latest_source_edge)
644
+ SET new_edge.from = $at
645
+ SET new_edge.branch_level = $branch_level
646
+ SET new_edge.branch = $target_branch
647
+ }
648
+ CALL {
649
+ WITH n, latest_source_edge, peer, edge_type
650
+ WITH n, latest_source_edge, peer, edge_type
651
+ WHERE edge_type = "HAS_ATTRIBUTE"
652
+ CREATE (n)-[new_edge:HAS_ATTRIBUTE]->(peer)
653
+ SET new_edge = properties(latest_source_edge)
654
+ SET new_edge.from = $at
655
+ SET new_edge.branch_level = $branch_level
656
+ SET new_edge.branch = $target_branch
657
+ }
658
+ // --------------
659
+ // do all of this again for inbound edges
660
+ // --------------
661
+ WITH DISTINCT n, peer
662
+ CALL {
663
+ // --------------
664
+ // get the latest inbound edge for each type between n and peer
665
+ // --------------
666
+ WITH n, peer
667
+ MATCH (n)<-[e {branch: $source_branch}]-(peer)
668
+ WHERE e.from <= $at AND e.to IS NULL
669
+ WITH e, type(e) AS edge_type
670
+ ORDER BY edge_type, e.from DESC
671
+ WITH edge_type, head(collect(e)) AS latest_source_edge
672
+ RETURN edge_type, latest_source_edge
673
+ }
674
+ CALL {
675
+ // --------------
676
+ // for each n, peer, edge_type, get the latest edge on target
677
+ // --------------
678
+ WITH n, peer, edge_type
679
+ OPTIONAL MATCH (n)<-[e {branch: $target_branch}]-(peer)
680
+ WHERE type(e) = edge_type AND e.from <= $at
681
+ RETURN e AS latest_target_edge
682
+ ORDER BY e.from DESC
683
+ LIMIT 1
684
+ }
685
+ // --------------
686
+ // ignore edges of this type that already have the correct status on the target branch
687
+ // --------------
688
+ WITH n, peer, edge_type, latest_source_edge, latest_target_edge
689
+ WHERE latest_target_edge IS NULL OR latest_source_edge.status <> latest_target_edge.status
690
+ CALL {
691
+ // --------------
692
+ // set the to time on active target branch edges that we are setting to deleted
693
+ // --------------
694
+ WITH latest_source_edge, latest_target_edge
695
+ WITH latest_source_edge, latest_target_edge
696
+ WHERE latest_target_edge IS NOT NULL
697
+ AND latest_source_edge.status = "deleted"
698
+ AND latest_target_edge.status = "active"
699
+ AND latest_target_edge.to IS NULL
700
+ SET latest_target_edge.to = $at
701
+ }
702
+ // --------------
703
+ // create the outbound edges on the target branch, one subquery per possible type
704
+ // --------------
705
+ CALL {
706
+ WITH n, latest_source_edge, peer, edge_type
707
+ WITH n, latest_source_edge, peer, edge_type
708
+ WHERE edge_type = "IS_RELATED"
709
+ CREATE (n)<-[new_edge:IS_RELATED]-(peer)
710
+ SET new_edge = properties(latest_source_edge)
711
+ SET new_edge.from = $at
712
+ SET new_edge.branch_level = $branch_level
713
+ SET new_edge.branch = $target_branch
714
+ }
715
+ CALL {
716
+ WITH n, latest_source_edge, peer, edge_type
717
+ WITH n, latest_source_edge, peer, edge_type
718
+ WHERE edge_type = "HAS_OWNER"
719
+ CREATE (n)<-[new_edge:HAS_OWNER]-(peer)
720
+ SET new_edge = properties(latest_source_edge)
721
+ SET new_edge.from = $at
722
+ SET new_edge.branch_level = $branch_level
723
+ SET new_edge.branch = $target_branch
724
+ }
725
+ CALL {
726
+ WITH n, latest_source_edge, peer, edge_type
727
+ WITH n, latest_source_edge, peer, edge_type
728
+ WHERE edge_type = "HAS_SOURCE"
729
+ CREATE (n)<-[new_edge:HAS_SOURCE]-(peer)
730
+ SET new_edge = properties(latest_source_edge)
731
+ SET new_edge.from = $at
732
+ SET new_edge.branch_level = $branch_level
733
+ SET new_edge.branch = $target_branch
734
+ }
735
+ }
736
+ """
737
+ self.add_to_query(query)
738
+
739
+
504
740
  class DiffMergeRollbackQuery(Query):
505
741
  name = "diff_merge_rollback"
506
742
  type = QueryType.WRITE
@@ -1,4 +1,3 @@
1
- import json
2
1
  from typing import Any, Iterable
3
2
 
4
3
  from infrahub.core.query import Query, QueryType
@@ -86,16 +85,17 @@ UNWIND $node_details_list AS node_details
86
85
  WITH
87
86
  node_details.root_uuid AS root_uuid,
88
87
  node_details.node_map AS node_map,
89
- toString(node_details.node_map.node_properties.uuid) AS node_uuid
88
+ toString(node_details.node_map.node_properties.uuid) AS node_uuid,
89
+ node_details.node_map.node_properties.db_id AS node_db_id
90
90
  MERGE (diff_root:DiffRoot {uuid: root_uuid})
91
- MERGE (diff_root)-[:DIFF_HAS_NODE]->(diff_node:DiffNode {uuid: node_uuid})
91
+ MERGE (diff_root)-[:DIFF_HAS_NODE]->(diff_node:DiffNode {uuid: node_uuid, db_id: node_db_id})
92
92
  WITH root_uuid, node_map, diff_node, (node_map.conflict_params IS NOT NULL) AS has_node_conflict
93
93
  SET
94
94
  diff_node.kind = node_map.node_properties.kind,
95
95
  diff_node.label = node_map.node_properties.label,
96
- diff_node.db_labels = node_map.node_properties.db_labels,
97
96
  diff_node.changed_at = node_map.node_properties.changed_at,
98
97
  diff_node.action = node_map.node_properties.action,
98
+ diff_node.is_node_kind_migration = node_map.node_properties.is_node_kind_migration,
99
99
  diff_node.path_identifier = node_map.node_properties.path_identifier
100
100
  WITH root_uuid, node_map, diff_node, has_node_conflict
101
101
  CALL {
@@ -403,7 +403,8 @@ FOREACH (i in CASE WHEN has_property_conflict = TRUE THEN [1] ELSE [] END |
403
403
  "node_properties": {
404
404
  "uuid": enriched_node.uuid,
405
405
  "kind": enriched_node.kind,
406
- "db_labels": json.dumps(list(enriched_node.identifier.labels)),
406
+ "db_id": enriched_node.identifier.db_id,
407
+ "is_node_kind_migration": enriched_node.is_node_kind_migration,
407
408
  "label": enriched_node.label,
408
409
  "changed_at": enriched_node.changed_at.to_string() if enriched_node.changed_at else None,
409
410
  "action": enriched_node.action.value,
@@ -558,7 +558,7 @@ class DiffQueryParser:
558
558
 
559
559
  def _get_diff_node(self, database_path: DatabasePath, diff_root: DiffRootIntermediate) -> DiffNodeIntermediate:
560
560
  identifier = NodeIdentifier(
561
- uuid=database_path.node_id, kind=database_path.node_kind, labels=database_path.node_labels
561
+ uuid=database_path.node_id, kind=database_path.node_kind, db_id=database_path.node_db_id
562
562
  )
563
563
  if identifier not in diff_root.nodes_by_identifier:
564
564
  diff_root.nodes_by_identifier[identifier] = DiffNodeIntermediate(
@@ -571,19 +571,6 @@ class DiffQueryParser:
571
571
  else None,
572
572
  )
573
573
  diff_node = diff_root.nodes_by_identifier[identifier]
574
- # special handling for nodes that have their kind updated, which results in 2 nodes with the same uuid
575
- if diff_node.db_id != database_path.node_db_id and (
576
- database_path.node_changed_at > diff_node.from_time
577
- or (
578
- database_path.node_changed_at >= diff_node.from_time
579
- and (diff_node.status, database_path.node_status)
580
- == (RelationshipStatus.DELETED, RelationshipStatus.ACTIVE)
581
- )
582
- ):
583
- diff_node.identifier.kind = database_path.node_kind
584
- diff_node.db_id = database_path.node_db_id
585
- diff_node.from_time = database_path.node_changed_at
586
- diff_node.status = database_path.node_status
587
574
  diff_node.track_database_path(database_path=database_path)
588
575
  return diff_node
589
576
 
@@ -697,8 +684,10 @@ class DiffQueryParser:
697
684
  branch_diff_root = self._diff_root_by_branch.get(branch)
698
685
  if not branch_diff_root:
699
686
  continue
687
+ base_diff_nodes_by_uuid = {n.uuid: n for n in base_diff_root.nodes_by_identifier.values()}
700
688
  for identifier, diff_node in branch_diff_root.nodes_by_identifier.items():
701
- base_diff_node = base_diff_root.nodes_by_identifier.get(identifier)
689
+ # changes on a base branch node with a given UUID should apply to all diff branch nodes with that UUID
690
+ base_diff_node = base_diff_nodes_by_uuid.get(identifier.uuid)
702
691
  if not base_diff_node:
703
692
  continue
704
693
  self._apply_attribute_previous_values(diff_node=diff_node, base_diff_node=base_diff_node)
@@ -1,5 +1,3 @@
1
- import json
2
-
3
1
  from neo4j.graph import Node as Neo4jNode
4
2
  from neo4j.graph import Path as Neo4jPath
5
3
 
@@ -86,11 +84,13 @@ class EnrichedDiffDeserializer:
86
84
  ) -> None:
87
85
  for attribute_result in result.get_nested_node_collection("diff_attributes"):
88
86
  diff_attr_node, diff_attr_property_node, diff_attr_property_conflict = attribute_result
89
- if diff_attr_node is None or diff_attr_property_node is None:
87
+ if diff_attr_node is None:
90
88
  continue
91
89
  enriched_attribute = self._deserialize_diff_attr(
92
90
  diff_attr_node=diff_attr_node, enriched_root=enriched_root, enriched_node=enriched_node
93
91
  )
92
+ if diff_attr_property_node is None:
93
+ continue
94
94
  enriched_property = self._deserialize_diff_attr_property(
95
95
  diff_attr_property_node=diff_attr_property_node,
96
96
  enriched_attr=enriched_attribute,
@@ -147,7 +147,7 @@ class EnrichedDiffDeserializer:
147
147
  parent_identifier = NodeIdentifier(
148
148
  uuid=parent.get("uuid"),
149
149
  kind=parent.get("kind"),
150
- labels=frozenset(json.loads(parent.get("db_labels"))),
150
+ db_id=parent.get("db_id"),
151
151
  )
152
152
  parent_request = ParentNodeAddRequest(
153
153
  node_identifier=current_node_identifier,
@@ -201,8 +201,8 @@ class EnrichedDiffDeserializer:
201
201
  def _deserialize_diff_node(self, node_node: Neo4jNode, enriched_root: EnrichedDiffRoot) -> EnrichedDiffNode:
202
202
  node_uuid = str(node_node.get("uuid"))
203
203
  node_kind = str(node_node.get("kind"))
204
- node_db_labels = frozenset(json.loads(node_node.get("db_labels")))
205
- node_identifier = NodeIdentifier(uuid=node_uuid, kind=node_kind, labels=node_db_labels)
204
+ node_db_id = node_node.get("db_id")
205
+ node_identifier = NodeIdentifier(uuid=node_uuid, kind=node_kind, db_id=node_db_id)
206
206
  node_key = (enriched_root.uuid, node_identifier)
207
207
  if node_key in self._diff_node_map:
208
208
  return self._diff_node_map[node_key]
@@ -213,6 +213,7 @@ class EnrichedDiffDeserializer:
213
213
  label=str(node_node.get("label")),
214
214
  changed_at=Timestamp(timestamp_str) if timestamp_str else None,
215
215
  action=DiffAction(str(node_node.get("action"))),
216
+ is_node_kind_migration=bool(node_node.get("is_node_kind_migration")),
216
217
  path_identifier=str(node_node.get("path_identifier")),
217
218
  num_added=int(node_node.get("num_added", 0)),
218
219
  num_updated=int(node_node.get("num_updated", 0)),
@@ -1 +1 @@
1
- GRAPH_VERSION = 27
1
+ GRAPH_VERSION = 28
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from infrahub.core import registry
6
+ from infrahub.core.diff.repository.repository import DiffRepository
7
+ from infrahub.core.migrations.shared import MigrationResult
8
+ from infrahub.dependencies.registry import build_component_registry, get_component_registry
9
+ from infrahub.log import get_logger
10
+
11
+ from ..shared import ArbitraryMigration
12
+
13
+ if TYPE_CHECKING:
14
+ from infrahub.database import InfrahubDatabase
15
+
16
+ log = get_logger()
17
+
18
+
19
+ class Migration028(ArbitraryMigration):
20
+ """Delete all diffs because of an update to how we store diff information. All diffs will need to be recalculated"""
21
+
22
+ name: str = "028_diff_delete_bug_fix_update"
23
+ minimum_version: int = 27
24
+
25
+ async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
26
+ result = MigrationResult()
27
+
28
+ return result
29
+
30
+ async def execute(self, db: InfrahubDatabase) -> MigrationResult:
31
+ default_branch = registry.get_branch_from_registry()
32
+ build_component_registry()
33
+ component_registry = get_component_registry()
34
+ diff_repo = await component_registry.get_component(DiffRepository, db=db, branch=default_branch)
35
+
36
+ diff_roots = await diff_repo.get_roots_metadata()
37
+ await diff_repo.delete_diff_roots(diff_root_uuids=[d.uuid for d in diff_roots])
38
+ return MigrationResult()