infrahub-server 1.2.9rc0__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 (52) hide show
  1. infrahub/computed_attribute/models.py +13 -0
  2. infrahub/computed_attribute/tasks.py +48 -26
  3. infrahub/core/attribute.py +43 -2
  4. infrahub/core/branch/models.py +8 -9
  5. infrahub/core/branch/tasks.py +0 -2
  6. infrahub/core/diff/calculator.py +65 -11
  7. infrahub/core/diff/combiner.py +38 -31
  8. infrahub/core/diff/coordinator.py +44 -28
  9. infrahub/core/diff/data_check_synchronizer.py +3 -2
  10. infrahub/core/diff/enricher/hierarchy.py +36 -27
  11. infrahub/core/diff/ipam_diff_parser.py +5 -4
  12. infrahub/core/diff/merger/merger.py +46 -16
  13. infrahub/core/diff/merger/serializer.py +1 -0
  14. infrahub/core/diff/model/field_specifiers_map.py +64 -0
  15. infrahub/core/diff/model/path.py +58 -58
  16. infrahub/core/diff/parent_node_adder.py +14 -16
  17. infrahub/core/diff/query/drop_nodes.py +42 -0
  18. infrahub/core/diff/query/field_specifiers.py +8 -7
  19. infrahub/core/diff/query/filters.py +15 -1
  20. infrahub/core/diff/query/merge.py +264 -28
  21. infrahub/core/diff/query/save.py +6 -2
  22. infrahub/core/diff/query_parser.py +50 -64
  23. infrahub/core/diff/repository/deserializer.py +38 -24
  24. infrahub/core/diff/repository/repository.py +31 -12
  25. infrahub/core/graph/__init__.py +1 -1
  26. infrahub/core/migrations/graph/__init__.py +2 -0
  27. infrahub/core/migrations/graph/m027_delete_isolated_nodes.py +50 -0
  28. infrahub/core/migrations/graph/m028_delete_diffs.py +38 -0
  29. infrahub/core/query/branch.py +27 -17
  30. infrahub/core/query/diff.py +162 -51
  31. infrahub/core/query/node.py +39 -5
  32. infrahub/core/query/relationship.py +105 -30
  33. infrahub/core/query/subquery.py +2 -2
  34. infrahub/core/relationship/model.py +1 -1
  35. infrahub/core/schema/schema_branch.py +3 -0
  36. infrahub/core/validators/uniqueness/query.py +7 -0
  37. infrahub/graphql/queries/diff/tree.py +2 -1
  38. infrahub/trigger/models.py +11 -1
  39. infrahub/trigger/setup.py +51 -15
  40. infrahub/trigger/tasks.py +1 -4
  41. infrahub/types.py +1 -1
  42. infrahub/webhook/models.py +2 -1
  43. infrahub/workflows/catalogue.py +9 -0
  44. infrahub/workflows/initialization.py +1 -3
  45. infrahub_sdk/timestamp.py +2 -2
  46. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.2.10.dist-info}/METADATA +3 -3
  47. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.2.10.dist-info}/RECORD +52 -48
  48. infrahub_testcontainers/docker-compose.test.yml +3 -3
  49. infrahub_testcontainers/performance_test.py +6 -3
  50. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.2.10.dist-info}/LICENSE.txt +0 -0
  51. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.2.10.dist-info}/WHEEL +0 -0
  52. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.2.10.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections import defaultdict
4
3
  from dataclasses import dataclass, field
5
4
  from typing import TYPE_CHECKING, Any
6
5
  from uuid import uuid4
@@ -15,6 +14,7 @@ from infrahub.core.constants import (
15
14
  from infrahub.core.constants.database import DatabaseEdgeType
16
15
  from infrahub.core.timestamp import Timestamp
17
16
 
17
+ from .model.field_specifiers_map import NodeFieldSpecifierMap
18
18
  from .model.path import (
19
19
  DatabasePath,
20
20
  DiffAttribute,
@@ -23,6 +23,7 @@ from .model.path import (
23
23
  DiffRelationship,
24
24
  DiffRoot,
25
25
  DiffSingleRelationship,
26
+ NodeIdentifier,
26
27
  )
27
28
 
28
29
  if TYPE_CHECKING:
@@ -394,8 +395,7 @@ class DiffRelationshipIntermediate:
394
395
  @dataclass
395
396
  class DiffNodeIntermediate(TrackedStatusUpdates):
396
397
  force_action: DiffAction | None
397
- uuid: str
398
- kind: str
398
+ identifier: NodeIdentifier
399
399
  db_id: str
400
400
  from_time: Timestamp
401
401
  status: RelationshipStatus
@@ -403,6 +403,14 @@ class DiffNodeIntermediate(TrackedStatusUpdates):
403
403
  # {(name, identifier): DiffRelationshipIntermediate}
404
404
  relationships_by_identifier: dict[tuple[str, str], DiffRelationshipIntermediate] = field(default_factory=dict)
405
405
 
406
+ @property
407
+ def uuid(self) -> str:
408
+ return self.identifier.uuid
409
+
410
+ @property
411
+ def kind(self) -> str:
412
+ return self.identifier.kind
413
+
406
414
  def to_diff_node(self, from_time: Timestamp, include_unchanged: bool) -> DiffNode:
407
415
  attributes = []
408
416
  for attr in self.attributes_by_name.values():
@@ -424,8 +432,7 @@ class DiffNodeIntermediate(TrackedStatusUpdates):
424
432
  if self.force_action:
425
433
  action = self.force_action
426
434
  return DiffNode(
427
- uuid=self.uuid,
428
- kind=self.kind,
435
+ identifier=self.identifier,
429
436
  changed_at=changed_at,
430
437
  action=action,
431
438
  attributes=attributes,
@@ -441,11 +448,11 @@ class DiffNodeIntermediate(TrackedStatusUpdates):
441
448
  class DiffRootIntermediate:
442
449
  uuid: str
443
450
  branch: str
444
- nodes_by_id: dict[str, DiffNodeIntermediate] = field(default_factory=dict)
451
+ nodes_by_identifier: dict[NodeIdentifier, DiffNodeIntermediate] = field(default_factory=dict)
445
452
 
446
453
  def to_diff_root(self, from_time: Timestamp, to_time: Timestamp, include_unchanged: bool) -> DiffRoot:
447
454
  nodes = []
448
- for node in self.nodes_by_id.values():
455
+ for node in self.nodes_by_identifier.values():
449
456
  if node.is_empty:
450
457
  continue
451
458
  diff_node = node.to_diff_node(from_time=from_time, include_unchanged=include_unchanged)
@@ -462,7 +469,7 @@ class DiffQueryParser:
462
469
  schema_manager: SchemaManager,
463
470
  from_time: Timestamp,
464
471
  to_time: Timestamp | None = None,
465
- previous_node_field_specifiers: dict[str, set[str]] | None = None,
472
+ previous_node_field_specifiers: NodeFieldSpecifierMap | None = None,
466
473
  ) -> None:
467
474
  self.base_branch_name = base_branch.name
468
475
  self.diff_branch_name = diff_branch.name
@@ -476,9 +483,9 @@ class DiffQueryParser:
476
483
  self.diff_branched_from_time = Timestamp(diff_branch.get_branched_from())
477
484
  self._diff_root_by_branch: dict[str, DiffRootIntermediate] = {}
478
485
  self._final_diff_root_by_branch: dict[str, DiffRoot] = {}
479
- self._previous_node_field_specifiers = previous_node_field_specifiers or {}
480
- self._new_node_field_specifiers: dict[str, set[str]] | None = None
481
- self._current_node_field_specifiers: dict[str, set[str]] | None = None
486
+ self._previous_node_field_specifiers = previous_node_field_specifiers or NodeFieldSpecifierMap()
487
+ self._new_node_field_specifiers: NodeFieldSpecifierMap | None = None
488
+ self._current_node_field_specifiers: NodeFieldSpecifierMap | None = None
482
489
 
483
490
  def get_branches(self) -> set[str]:
484
491
  return set(self._final_diff_root_by_branch.keys())
@@ -490,48 +497,33 @@ class DiffQueryParser:
490
497
  return self._final_diff_root_by_branch[branch]
491
498
  return DiffRoot(from_time=self.from_time, to_time=self.to_time, uuid=str(uuid4()), branch=branch, nodes=[])
492
499
 
493
- def get_diff_node_field_specifiers(self) -> dict[str, set[str]]:
500
+ def get_diff_node_field_specifiers(self) -> NodeFieldSpecifierMap:
501
+ node_field_specifiers_map = NodeFieldSpecifierMap()
494
502
  if self.diff_branch_name not in self._diff_root_by_branch:
495
- return {}
496
- node_field_specifiers_map: dict[str, set[str]] = defaultdict(set)
503
+ return node_field_specifiers_map
497
504
  diff_root = self._diff_root_by_branch[self.diff_branch_name]
498
- for node in diff_root.nodes_by_id.values():
505
+ for node in diff_root.nodes_by_identifier.values():
499
506
  for attribute_name in node.attributes_by_name:
500
- node_field_specifiers_map[node.uuid].add(attribute_name)
507
+ node_field_specifiers_map.add_entry(node_uuid=node.uuid, kind=node.kind, field_name=attribute_name)
501
508
  for relationship_diff in node.relationships_by_identifier.values():
502
- node_field_specifiers_map[node.uuid].add(relationship_diff.identifier)
509
+ node_field_specifiers_map.add_entry(
510
+ node_uuid=node.uuid, kind=node.kind, field_name=relationship_diff.identifier
511
+ )
503
512
  return node_field_specifiers_map
504
513
 
505
- def _remove_node_specifiers(
506
- self, node_specifiers: dict[str, set[str]], node_specifiers_to_remove: dict[str, set[str]]
507
- ) -> dict[str, set[str]]:
508
- final_node_specifiers: dict[str, set[str]] = defaultdict(set)
509
- for node_uuid, field_names_set in node_specifiers.items():
510
- specifiers_to_remove = node_specifiers_to_remove.get(node_uuid, set())
511
- final_specifiers = field_names_set - specifiers_to_remove
512
- if final_specifiers:
513
- final_node_specifiers[node_uuid] = final_specifiers
514
- return final_node_specifiers
515
-
516
- def get_new_node_field_specifiers(self) -> dict[str, set[str]]:
514
+ def get_new_node_field_specifiers(self) -> NodeFieldSpecifierMap:
517
515
  if self._new_node_field_specifiers is not None:
518
516
  return self._new_node_field_specifiers
519
517
  branch_node_specifiers = self.get_diff_node_field_specifiers()
520
- new_node_field_specifiers = self._remove_node_specifiers(
521
- branch_node_specifiers, self._previous_node_field_specifiers
522
- )
523
- self._new_node_field_specifiers = new_node_field_specifiers
524
- return new_node_field_specifiers
518
+ self._new_node_field_specifiers = branch_node_specifiers - self._previous_node_field_specifiers
519
+ return self._new_node_field_specifiers
525
520
 
526
- def get_current_node_field_specifiers(self) -> dict[str, set[str]]:
521
+ def get_current_node_field_specifiers(self) -> NodeFieldSpecifierMap:
527
522
  if self._current_node_field_specifiers is not None:
528
523
  return self._current_node_field_specifiers
529
524
  new_node_field_specifiers = self.get_new_node_field_specifiers()
530
- current_node_field_specifiers = self._remove_node_specifiers(
531
- self._previous_node_field_specifiers, new_node_field_specifiers
532
- )
533
- self._current_node_field_specifiers = current_node_field_specifiers
534
- return current_node_field_specifiers
525
+ self._current_node_field_specifiers = self._previous_node_field_specifiers - new_node_field_specifiers
526
+ return self._current_node_field_specifiers
535
527
 
536
528
  def read_result(self, query_result: QueryResult) -> None:
537
529
  path = query_result.get_path(label="diff_path")
@@ -565,11 +557,12 @@ class DiffQueryParser:
565
557
  return self._diff_root_by_branch[branch]
566
558
 
567
559
  def _get_diff_node(self, database_path: DatabasePath, diff_root: DiffRootIntermediate) -> DiffNodeIntermediate:
568
- node_id = database_path.node_id
569
- if node_id not in diff_root.nodes_by_id:
570
- diff_root.nodes_by_id[node_id] = DiffNodeIntermediate(
571
- uuid=node_id,
572
- kind=database_path.node_kind,
560
+ identifier = NodeIdentifier(
561
+ uuid=database_path.node_id, kind=database_path.node_kind, db_id=database_path.node_db_id
562
+ )
563
+ if identifier not in diff_root.nodes_by_identifier:
564
+ diff_root.nodes_by_identifier[identifier] = DiffNodeIntermediate(
565
+ identifier=identifier,
573
566
  db_id=database_path.node_db_id,
574
567
  from_time=database_path.node_changed_at,
575
568
  status=database_path.node_status,
@@ -577,20 +570,7 @@ class DiffQueryParser:
577
570
  if database_path.node_branch_support is BranchSupportType.AGNOSTIC
578
571
  else None,
579
572
  )
580
- diff_node = diff_root.nodes_by_id[node_id]
581
- # special handling for nodes that have their kind updated, which results in 2 nodes with the same uuid
582
- if diff_node.db_id != database_path.node_db_id and (
583
- database_path.node_changed_at > diff_node.from_time
584
- or (
585
- database_path.node_changed_at >= diff_node.from_time
586
- and (diff_node.status, database_path.node_status)
587
- == (RelationshipStatus.DELETED, RelationshipStatus.ACTIVE)
588
- )
589
- ):
590
- diff_node.kind = database_path.node_kind
591
- diff_node.db_id = database_path.node_db_id
592
- diff_node.from_time = database_path.node_changed_at
593
- diff_node.status = database_path.node_status
573
+ diff_node = diff_root.nodes_by_identifier[identifier]
594
574
  diff_node.track_database_path(database_path=database_path)
595
575
  return diff_node
596
576
 
@@ -634,7 +614,9 @@ class DiffQueryParser:
634
614
  from_time = self.from_time
635
615
  if branch_name == self.base_branch_name:
636
616
  new_node_field_specifiers = self.get_new_node_field_specifiers()
637
- if attribute_name in new_node_field_specifiers.get(diff_node.uuid, set()):
617
+ if new_node_field_specifiers.has_entry(
618
+ node_uuid=diff_node.uuid, kind=diff_node.kind, field_name=attribute_name
619
+ ):
638
620
  from_time = self.diff_branched_from_time
639
621
  if attribute_name not in diff_node.attributes_by_name:
640
622
  diff_node.attributes_by_name[attribute_name] = DiffAttributeIntermediate(
@@ -678,7 +660,9 @@ class DiffQueryParser:
678
660
  from_time = self.from_time
679
661
  if branch_name == self.base_branch_name:
680
662
  new_node_field_specifiers = self.get_new_node_field_specifiers()
681
- if relationship_schema.get_identifier() in new_node_field_specifiers.get(diff_node.uuid, set()):
663
+ if new_node_field_specifiers.has_entry(
664
+ node_uuid=diff_node.uuid, kind=diff_node.kind, field_name=relationship_schema.get_identifier()
665
+ ):
682
666
  from_time = self.diff_branched_from_time
683
667
  diff_relationship = DiffRelationshipIntermediate(
684
668
  name=relationship_schema.name,
@@ -700,8 +684,10 @@ class DiffQueryParser:
700
684
  branch_diff_root = self._diff_root_by_branch.get(branch)
701
685
  if not branch_diff_root:
702
686
  continue
703
- for node_id, diff_node in branch_diff_root.nodes_by_id.items():
704
- base_diff_node = base_diff_root.nodes_by_id.get(node_id)
687
+ base_diff_nodes_by_uuid = {n.uuid: n for n in base_diff_root.nodes_by_identifier.values()}
688
+ for identifier, diff_node in branch_diff_root.nodes_by_identifier.items():
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)
705
691
  if not base_diff_node:
706
692
  continue
707
693
  self._apply_attribute_previous_values(diff_node=diff_node, base_diff_node=base_diff_node)
@@ -771,7 +757,7 @@ class DiffQueryParser:
771
757
  base_diff_root = self._diff_root_by_branch.get(self.base_branch_name)
772
758
  if not base_diff_root:
773
759
  return
774
- for node_diff in base_diff_root.nodes_by_id.values():
760
+ for node_diff in base_diff_root.nodes_by_identifier.values():
775
761
  for attribute_diff in node_diff.attributes_by_name.values():
776
762
  for property_diff in attribute_diff.properties_by_type.values():
777
763
  ordered_diff_values = property_diff.get_ordered_values_asc()
@@ -16,6 +16,7 @@ from ..model.path import (
16
16
  EnrichedDiffRoot,
17
17
  EnrichedDiffRootMetadata,
18
18
  EnrichedDiffSingleRelationship,
19
+ NodeIdentifier,
19
20
  deserialize_tracking_id,
20
21
  )
21
22
  from ..parent_node_adder import DiffParentNodeAdder, ParentNodeAddRequest
@@ -25,13 +26,15 @@ class EnrichedDiffDeserializer:
25
26
  def __init__(self, parent_adder: DiffParentNodeAdder) -> None:
26
27
  self.parent_adder = parent_adder
27
28
  self._diff_root_map: dict[str, EnrichedDiffRoot] = {}
28
- self._diff_node_map: dict[tuple[str, str], EnrichedDiffNode] = {}
29
- self._diff_node_attr_map: dict[tuple[str, str, str], EnrichedDiffAttribute] = {}
30
- self._diff_node_rel_group_map: dict[tuple[str, str, str], EnrichedDiffRelationship] = {}
31
- self._diff_node_rel_element_map: dict[tuple[str, str, str, str], EnrichedDiffSingleRelationship] = {}
32
- self._diff_prop_map: dict[tuple[str, str, str, str] | tuple[str, str, str, str, str], EnrichedDiffProperty] = {}
33
- # {EnrichedDiffRoot: [(node_uuid, parents_path: Neo4jPath), ...]}
34
- self._parents_path_map: dict[EnrichedDiffRoot, list[tuple[str, Neo4jPath]]] = {}
29
+ self._diff_node_map: dict[tuple[str, NodeIdentifier], EnrichedDiffNode] = {}
30
+ self._diff_node_attr_map: dict[tuple[str, NodeIdentifier, str], EnrichedDiffAttribute] = {}
31
+ self._diff_node_rel_group_map: dict[tuple[str, NodeIdentifier, str], EnrichedDiffRelationship] = {}
32
+ self._diff_node_rel_element_map: dict[tuple[str, NodeIdentifier, str, str], EnrichedDiffSingleRelationship] = {}
33
+ self._diff_prop_map: dict[
34
+ tuple[str, NodeIdentifier, str, str] | tuple[str, str, str, str, str], EnrichedDiffProperty
35
+ ] = {}
36
+ # {EnrichedDiffRoot: [(NodeIdentifier, parents_path: Neo4jPath), ...]}
37
+ self._parents_path_map: dict[EnrichedDiffRoot, list[tuple[NodeIdentifier, Neo4jPath]]] = {}
35
38
 
36
39
  def initialize(self) -> None:
37
40
  self._diff_root_map = {}
@@ -42,10 +45,12 @@ class EnrichedDiffDeserializer:
42
45
  self._diff_prop_map = {}
43
46
  self._parents_path_map = {}
44
47
 
45
- def _track_parents_path(self, enriched_root: EnrichedDiffRoot, node_uuid: str, parents_path: Neo4jPath) -> None:
48
+ def _track_parents_path(
49
+ self, enriched_root: EnrichedDiffRoot, node_identifier: NodeIdentifier, parents_path: Neo4jPath
50
+ ) -> None:
46
51
  if enriched_root not in self._parents_path_map:
47
52
  self._parents_path_map[enriched_root] = []
48
- self._parents_path_map[enriched_root].append((node_uuid, parents_path))
53
+ self._parents_path_map[enriched_root].append((node_identifier, parents_path))
49
54
 
50
55
  async def read_result(self, result: QueryResult, include_parents: bool) -> None:
51
56
  enriched_root = self._deserialize_diff_root(root_node=result.get_node("diff_root"))
@@ -58,7 +63,7 @@ class EnrichedDiffDeserializer:
58
63
  parents_path = result.get("parents_path")
59
64
  if parents_path and isinstance(parents_path, Neo4jPath):
60
65
  self._track_parents_path(
61
- enriched_root=enriched_root, node_uuid=enriched_node.uuid, parents_path=parents_path
66
+ enriched_root=enriched_root, node_identifier=enriched_node.identifier, parents_path=parents_path
62
67
  )
63
68
 
64
69
  node_conflict_node = result.get(label="diff_node_conflict")
@@ -79,11 +84,13 @@ class EnrichedDiffDeserializer:
79
84
  ) -> None:
80
85
  for attribute_result in result.get_nested_node_collection("diff_attributes"):
81
86
  diff_attr_node, diff_attr_property_node, diff_attr_property_conflict = attribute_result
82
- if diff_attr_node is None or diff_attr_property_node is None:
87
+ if diff_attr_node is None:
83
88
  continue
84
89
  enriched_attribute = self._deserialize_diff_attr(
85
90
  diff_attr_node=diff_attr_node, enriched_root=enriched_root, enriched_node=enriched_node
86
91
  )
92
+ if diff_attr_property_node is None:
93
+ continue
87
94
  enriched_property = self._deserialize_diff_attr_property(
88
95
  diff_attr_property_node=diff_attr_property_node,
89
96
  enriched_attr=enriched_attribute,
@@ -130,17 +137,21 @@ class EnrichedDiffDeserializer:
130
137
  def _deserialize_parents(self) -> None:
131
138
  for enriched_root, node_path_tuples in self._parents_path_map.items():
132
139
  self.parent_adder.initialize(enriched_diff_root=enriched_root)
133
- for node_uuid, parents_path in node_path_tuples:
140
+ for node_identifier, parents_path in node_path_tuples:
134
141
  # Remove the node itself from the path
135
142
  parents_path_slice = parents_path.nodes[1:]
136
143
 
137
144
  # TODO Ensure the list is even
138
- current_node_uuid = node_uuid
145
+ current_node_identifier = node_identifier
139
146
  for rel, parent in zip(parents_path_slice[::2], parents_path_slice[1::2], strict=False):
147
+ parent_identifier = NodeIdentifier(
148
+ uuid=parent.get("uuid"),
149
+ kind=parent.get("kind"),
150
+ db_id=parent.get("db_id"),
151
+ )
140
152
  parent_request = ParentNodeAddRequest(
141
- node_id=current_node_uuid,
142
- parent_id=parent.get("uuid"),
143
- parent_kind=parent.get("kind"),
153
+ node_identifier=current_node_identifier,
154
+ parent_identifier=parent_identifier,
144
155
  parent_label=parent.get("label"),
145
156
  parent_rel_name=rel.get("name"),
146
157
  parent_rel_identifier=rel.get("identifier"),
@@ -148,7 +159,7 @@ class EnrichedDiffDeserializer:
148
159
  parent_rel_label=rel.get("label"),
149
160
  )
150
161
  self.parent_adder.add_parent(parent_request=parent_request)
151
- current_node_uuid = parent.get("uuid")
162
+ current_node_identifier = parent_identifier
152
163
 
153
164
  @classmethod
154
165
  def _get_str_or_none_property_value(cls, node: Neo4jNode, property_name: str) -> str | None:
@@ -189,17 +200,20 @@ class EnrichedDiffDeserializer:
189
200
 
190
201
  def _deserialize_diff_node(self, node_node: Neo4jNode, enriched_root: EnrichedDiffRoot) -> EnrichedDiffNode:
191
202
  node_uuid = str(node_node.get("uuid"))
192
- node_key = (enriched_root.uuid, node_uuid)
203
+ node_kind = str(node_node.get("kind"))
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
+ node_key = (enriched_root.uuid, node_identifier)
193
207
  if node_key in self._diff_node_map:
194
208
  return self._diff_node_map[node_key]
195
209
 
196
210
  timestamp_str = self._get_str_or_none_property_value(node=node_node, property_name="changed_at")
197
211
  enriched_node = EnrichedDiffNode(
198
- uuid=node_uuid,
199
- kind=str(node_node.get("kind")),
212
+ identifier=node_identifier,
200
213
  label=str(node_node.get("label")),
201
214
  changed_at=Timestamp(timestamp_str) if timestamp_str else None,
202
215
  action=DiffAction(str(node_node.get("action"))),
216
+ is_node_kind_migration=bool(node_node.get("is_node_kind_migration")),
203
217
  path_identifier=str(node_node.get("path_identifier")),
204
218
  num_added=int(node_node.get("num_added", 0)),
205
219
  num_updated=int(node_node.get("num_updated", 0)),
@@ -215,7 +229,7 @@ class EnrichedDiffDeserializer:
215
229
  self, diff_attr_node: Neo4jNode, enriched_root: EnrichedDiffRoot, enriched_node: EnrichedDiffNode
216
230
  ) -> EnrichedDiffAttribute:
217
231
  attr_name = str(diff_attr_node.get("name"))
218
- attr_key = (enriched_root.uuid, enriched_node.uuid, attr_name)
232
+ attr_key = (enriched_root.uuid, enriched_node.identifier, attr_name)
219
233
  if attr_key in self._diff_node_attr_map:
220
234
  return self._diff_node_attr_map[attr_key]
221
235
 
@@ -238,7 +252,7 @@ class EnrichedDiffDeserializer:
238
252
  self, relationship_group_node: Neo4jNode, enriched_root: EnrichedDiffRoot, enriched_node: EnrichedDiffNode
239
253
  ) -> EnrichedDiffRelationship:
240
254
  diff_rel_name = str(relationship_group_node.get("name"))
241
- rel_key = (enriched_root.uuid, enriched_node.uuid, diff_rel_name)
255
+ rel_key = (enriched_root.uuid, enriched_node.identifier, diff_rel_name)
242
256
  if rel_key in self._diff_node_rel_group_map:
243
257
  return self._diff_node_rel_group_map[rel_key]
244
258
 
@@ -272,7 +286,7 @@ class EnrichedDiffDeserializer:
272
286
  diff_element_peer_id = str(relationship_element_node.get("peer_id"))
273
287
  rel_element_key = (
274
288
  enriched_root.uuid,
275
- enriched_node.uuid,
289
+ enriched_node.identifier,
276
290
  enriched_relationship_group.name,
277
291
  diff_element_peer_id,
278
292
  )
@@ -320,7 +334,7 @@ class EnrichedDiffDeserializer:
320
334
  enriched_root: EnrichedDiffRoot,
321
335
  ) -> EnrichedDiffProperty:
322
336
  diff_prop_type = str(diff_attr_property_node.get("property_type"))
323
- attr_property_key = (enriched_root.uuid, enriched_node.uuid, enriched_attr.name, diff_prop_type)
337
+ attr_property_key = (enriched_root.uuid, enriched_node.identifier, enriched_attr.name, diff_prop_type)
324
338
  if attr_property_key in self._diff_prop_map:
325
339
  return self._diff_prop_map[attr_property_key]
326
340
 
@@ -1,10 +1,10 @@
1
- from collections import defaultdict
2
1
  from typing import AsyncGenerator, Generator, Iterable
3
2
 
4
3
  from neo4j.exceptions import TransientError
5
4
 
6
5
  from infrahub import config
7
6
  from infrahub.core import registry
7
+ from infrahub.core.diff.query.drop_nodes import EnrichedDiffDropNodesQuery
8
8
  from infrahub.core.diff.query.field_summary import EnrichedDiffNodeFieldSummaryQuery
9
9
  from infrahub.core.diff.query.summary_counts_enricher import (
10
10
  DiffFieldsSummaryCountsEnricherQuery,
@@ -16,6 +16,7 @@ from infrahub.database import InfrahubDatabase, retry_db_transaction
16
16
  from infrahub.exceptions import ResourceNotFoundError
17
17
  from infrahub.log import get_logger
18
18
 
19
+ from ..model.field_specifiers_map import NodeFieldSpecifierMap
19
20
  from ..model.path import (
20
21
  ConflictSelection,
21
22
  EnrichedDiffConflict,
@@ -26,6 +27,7 @@ from ..model.path import (
26
27
  EnrichedDiffsMetadata,
27
28
  EnrichedNodeCreateRequest,
28
29
  NodeDiffFieldSummary,
30
+ NodeIdentifier,
29
31
  TimeRange,
30
32
  TrackingId,
31
33
  )
@@ -108,7 +110,7 @@ class DiffRepository:
108
110
  diff_branch_names: list[str],
109
111
  from_time: Timestamp | None = None,
110
112
  to_time: Timestamp | None = None,
111
- filters: dict | None = None,
113
+ filters: EnrichedDiffQueryFilters | None = None,
112
114
  include_parents: bool = True,
113
115
  limit: int | None = None,
114
116
  offset: int | None = None,
@@ -180,11 +182,11 @@ class DiffRepository:
180
182
  async def hydrate_diff_pair(
181
183
  self,
182
184
  enriched_diffs_metadata: EnrichedDiffsMetadata,
183
- node_uuids: Iterable[str] | None = None,
185
+ node_identifiers: Iterable[NodeIdentifier] | None = None,
184
186
  ) -> EnrichedDiffs:
185
- filters = None
186
- if node_uuids:
187
- filters = {"ids": list(node_uuids) if node_uuids is not None else None}
187
+ filters = EnrichedDiffQueryFilters()
188
+ if node_identifiers:
189
+ filters.identifiers = list(node_identifiers)
188
190
  hydrated_base_diff = await self.get_one(
189
191
  diff_branch_name=enriched_diffs_metadata.base_branch_name,
190
192
  diff_id=enriched_diffs_metadata.base_branch_diff.uuid,
@@ -207,7 +209,7 @@ class DiffRepository:
207
209
  diff_branch_name: str,
208
210
  tracking_id: TrackingId | None = None,
209
211
  diff_id: str | None = None,
210
- filters: dict | None = None,
212
+ filters: EnrichedDiffQueryFilters | None = None,
211
213
  include_parents: bool = True,
212
214
  ) -> EnrichedDiffRoot:
213
215
  enriched_diffs = await self.get(
@@ -274,6 +276,12 @@ class DiffRepository:
274
276
  )
275
277
  await single_node_query.execute(db=self.db)
276
278
 
279
+ async def _drop_nodes(self, diff_root: EnrichedDiffRoot, node_identifiers: list[NodeIdentifier]) -> None:
280
+ drop_node_query = await EnrichedDiffDropNodesQuery.init(
281
+ db=self.db, enriched_diff_uuid=diff_root.uuid, node_identifiers=node_identifiers
282
+ )
283
+ await drop_node_query.execute(db=self.db)
284
+
277
285
  @retry_db_transaction(name="enriched_diff_hierarchy_update")
278
286
  async def _run_hierarchy_links_update_query(self, diff_root_uuid: str, diff_nodes: list[EnrichedDiffNode]) -> None:
279
287
  log.info(f"Updating diff hierarchy links, num_nodes={len(diff_nodes)}")
@@ -321,7 +329,12 @@ class DiffRepository:
321
329
  node_uuids=node_uuids,
322
330
  )
323
331
 
324
- async def save(self, enriched_diffs: EnrichedDiffs | EnrichedDiffsMetadata, do_summary_counts: bool = True) -> None:
332
+ async def save(
333
+ self,
334
+ enriched_diffs: EnrichedDiffs | EnrichedDiffsMetadata,
335
+ do_summary_counts: bool = True,
336
+ node_identifiers_to_drop: list[NodeIdentifier] | None = None,
337
+ ) -> None:
325
338
  # metadata-only update
326
339
  if not isinstance(enriched_diffs, EnrichedDiffs):
327
340
  await self._save_root_metadata(enriched_diffs=enriched_diffs)
@@ -336,6 +349,8 @@ class DiffRepository:
336
349
  await self._save_node_batch(node_create_batch=node_create_batch)
337
350
  count_nodes_remaining -= len(node_create_batch)
338
351
  log.info(f"Batch saved. {count_nodes_remaining=}")
352
+ if node_identifiers_to_drop:
353
+ await self._drop_nodes(diff_root=enriched_diffs.diff_branch_diff, node_identifiers=node_identifiers_to_drop)
339
354
  await self._update_hierarchy_links(enriched_diffs=enriched_diffs)
340
355
  if do_summary_counts:
341
356
  await self._update_summary_counts(diff_root=enriched_diffs.diff_branch_diff)
@@ -501,21 +516,25 @@ class DiffRepository:
501
516
  await query.execute(db=self.db)
502
517
  return query.get_num_changes_by_branch()
503
518
 
504
- async def get_node_field_specifiers(self, diff_id: str) -> dict[str, set[str]]:
519
+ async def get_node_field_specifiers(self, diff_id: str) -> NodeFieldSpecifierMap:
505
520
  limit = config.SETTINGS.database.query_size_limit
506
521
  offset = 0
507
- specifiers: dict[str, set[str]] = defaultdict(set)
522
+ specifiers_map = NodeFieldSpecifierMap()
508
523
  while True:
509
524
  query = await EnrichedDiffFieldSpecifiersQuery.init(db=self.db, diff_id=diff_id, offset=offset, limit=limit)
510
525
  await query.execute(db=self.db)
511
526
  has_data = False
512
527
  for field_specifier_tuple in query.get_node_field_specifier_tuples():
513
- specifiers[field_specifier_tuple[0]].add(field_specifier_tuple[1])
528
+ specifiers_map.add_entry(
529
+ node_uuid=field_specifier_tuple[0],
530
+ kind=field_specifier_tuple[1],
531
+ field_name=field_specifier_tuple[2],
532
+ )
514
533
  has_data = True
515
534
  if not has_data:
516
535
  break
517
536
  offset += limit
518
- return specifiers
537
+ return specifiers_map
519
538
 
520
539
  async def add_summary_counts(
521
540
  self,
@@ -1 +1 @@
1
- GRAPH_VERSION = 26
1
+ GRAPH_VERSION = 28
@@ -28,6 +28,7 @@ from .m023_deduplicate_cardinality_one_relationships import Migration023
28
28
  from .m024_missing_hierarchy_backfill import Migration024
29
29
  from .m025_uniqueness_nulls import Migration025
30
30
  from .m026_0000_prefix_fix import Migration026
31
+ from .m027_delete_isolated_nodes import Migration027
31
32
 
32
33
  if TYPE_CHECKING:
33
34
  from infrahub.core.root import Root
@@ -61,6 +62,7 @@ MIGRATIONS: list[type[GraphMigration | InternalSchemaMigration | ArbitraryMigrat
61
62
  Migration024,
62
63
  Migration025,
63
64
  Migration026,
65
+ Migration027,
64
66
  ]
65
67
 
66
68
 
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Sequence
4
+
5
+ from infrahub.core.migrations.shared import GraphMigration, MigrationResult
6
+ from infrahub.log import get_logger
7
+
8
+ from ...query import Query, QueryType
9
+
10
+ if TYPE_CHECKING:
11
+ from infrahub.database import InfrahubDatabase
12
+
13
+ log = get_logger()
14
+
15
+
16
+ class DeleteIsolatedNodesQuery(Query):
17
+ name = "delete_isolated_nodes_query"
18
+ type = QueryType.WRITE
19
+ insert_return = False
20
+
21
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
22
+ query = """
23
+ MATCH p = (s: Node)-[r]-(d)
24
+ WHERE NOT exists((s)-[:IS_PART_OF]-(:Root))
25
+ DELETE r
26
+
27
+ WITH p
28
+ UNWIND nodes(p) AS n
29
+ MATCH (n)
30
+ WHERE NOT exists((n)--())
31
+ DELETE n
32
+ """
33
+ self.add_to_query(query)
34
+
35
+
36
+ class Migration027(GraphMigration):
37
+ """
38
+ While deleting a branch containing some allocated nodes from a resource pool, relationship
39
+ between pool node and resource node might be agnostic (eg: for IPPrefixPool) and incorrectly deleted,
40
+ resulting in a node still linked to the resource pool but not linked to Root anymore.
41
+ This query deletes nodes not linked to Root and their relationships (supposed to be agnostic).
42
+ """
43
+
44
+ name: str = "027_deleted_isolated_nodes"
45
+ minimum_version: int = 26
46
+ queries: Sequence[type[Query]] = [DeleteIsolatedNodesQuery]
47
+
48
+ async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
49
+ result = MigrationResult()
50
+ return result
@@ -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()