infrahub-server 1.2.9rc0__py3-none-any.whl → 1.3.0a0__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 (166) hide show
  1. infrahub/actions/constants.py +86 -0
  2. infrahub/actions/gather.py +114 -0
  3. infrahub/actions/models.py +241 -0
  4. infrahub/actions/parsers.py +104 -0
  5. infrahub/actions/schema.py +382 -0
  6. infrahub/actions/tasks.py +126 -0
  7. infrahub/actions/triggers.py +21 -0
  8. infrahub/cli/db.py +1 -2
  9. infrahub/computed_attribute/models.py +13 -0
  10. infrahub/computed_attribute/tasks.py +48 -26
  11. infrahub/config.py +9 -0
  12. infrahub/core/account.py +24 -47
  13. infrahub/core/attribute.py +53 -14
  14. infrahub/core/branch/models.py +8 -9
  15. infrahub/core/branch/tasks.py +0 -2
  16. infrahub/core/constants/infrahubkind.py +8 -0
  17. infrahub/core/constraint/node/runner.py +1 -1
  18. infrahub/core/convert_object_type/__init__.py +0 -0
  19. infrahub/core/convert_object_type/conversion.py +122 -0
  20. infrahub/core/convert_object_type/schema_mapping.py +56 -0
  21. infrahub/core/diff/calculator.py +65 -11
  22. infrahub/core/diff/combiner.py +38 -31
  23. infrahub/core/diff/coordinator.py +44 -28
  24. infrahub/core/diff/data_check_synchronizer.py +3 -2
  25. infrahub/core/diff/enricher/hierarchy.py +36 -27
  26. infrahub/core/diff/ipam_diff_parser.py +5 -4
  27. infrahub/core/diff/merger/merger.py +46 -16
  28. infrahub/core/diff/merger/serializer.py +1 -0
  29. infrahub/core/diff/model/field_specifiers_map.py +64 -0
  30. infrahub/core/diff/model/path.py +58 -58
  31. infrahub/core/diff/parent_node_adder.py +14 -16
  32. infrahub/core/diff/query/all_conflicts.py +1 -5
  33. infrahub/core/diff/query/artifact.py +10 -20
  34. infrahub/core/diff/query/diff_get.py +3 -6
  35. infrahub/core/diff/query/drop_nodes.py +42 -0
  36. infrahub/core/diff/query/field_specifiers.py +8 -7
  37. infrahub/core/diff/query/field_summary.py +2 -4
  38. infrahub/core/diff/query/filters.py +15 -1
  39. infrahub/core/diff/query/merge.py +284 -101
  40. infrahub/core/diff/query/save.py +26 -34
  41. infrahub/core/diff/query/summary_counts_enricher.py +34 -54
  42. infrahub/core/diff/query_parser.py +55 -65
  43. infrahub/core/diff/repository/deserializer.py +38 -24
  44. infrahub/core/diff/repository/repository.py +31 -12
  45. infrahub/core/diff/tasks.py +3 -3
  46. infrahub/core/graph/__init__.py +1 -1
  47. infrahub/core/manager.py +14 -11
  48. infrahub/core/migrations/graph/__init__.py +2 -0
  49. infrahub/core/migrations/graph/m003_relationship_parent_optional.py +1 -2
  50. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +2 -4
  51. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +11 -22
  52. infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -6
  53. infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +1 -2
  54. infrahub/core/migrations/graph/m024_missing_hierarchy_backfill.py +1 -2
  55. infrahub/core/migrations/graph/m027_delete_isolated_nodes.py +50 -0
  56. infrahub/core/migrations/graph/m028_delete_diffs.py +38 -0
  57. infrahub/core/migrations/query/attribute_add.py +1 -2
  58. infrahub/core/migrations/query/attribute_rename.py +3 -6
  59. infrahub/core/migrations/query/delete_element_in_schema.py +3 -6
  60. infrahub/core/migrations/query/node_duplicate.py +3 -6
  61. infrahub/core/migrations/query/relationship_duplicate.py +3 -6
  62. infrahub/core/migrations/schema/node_attribute_remove.py +3 -6
  63. infrahub/core/migrations/schema/node_remove.py +3 -6
  64. infrahub/core/models.py +29 -2
  65. infrahub/core/node/__init__.py +18 -4
  66. infrahub/core/node/create.py +211 -0
  67. infrahub/core/protocols.py +51 -0
  68. infrahub/core/protocols_base.py +3 -0
  69. infrahub/core/query/__init__.py +2 -2
  70. infrahub/core/query/branch.py +27 -17
  71. infrahub/core/query/diff.py +186 -81
  72. infrahub/core/query/ipam.py +10 -20
  73. infrahub/core/query/node.py +65 -49
  74. infrahub/core/query/relationship.py +156 -58
  75. infrahub/core/query/resource_manager.py +1 -2
  76. infrahub/core/query/subquery.py +4 -6
  77. infrahub/core/relationship/model.py +4 -1
  78. infrahub/core/schema/__init__.py +2 -1
  79. infrahub/core/schema/attribute_parameters.py +36 -0
  80. infrahub/core/schema/attribute_schema.py +83 -8
  81. infrahub/core/schema/basenode_schema.py +25 -1
  82. infrahub/core/schema/definitions/core/__init__.py +21 -0
  83. infrahub/core/schema/definitions/internal.py +13 -3
  84. infrahub/core/schema/generated/attribute_schema.py +9 -3
  85. infrahub/core/schema/schema_branch.py +15 -7
  86. infrahub/core/validators/__init__.py +5 -1
  87. infrahub/core/validators/attribute/choices.py +1 -2
  88. infrahub/core/validators/attribute/enum.py +1 -2
  89. infrahub/core/validators/attribute/kind.py +1 -2
  90. infrahub/core/validators/attribute/length.py +13 -6
  91. infrahub/core/validators/attribute/optional.py +1 -2
  92. infrahub/core/validators/attribute/regex.py +5 -5
  93. infrahub/core/validators/attribute/unique.py +1 -3
  94. infrahub/core/validators/determiner.py +18 -2
  95. infrahub/core/validators/enum.py +7 -0
  96. infrahub/core/validators/node/hierarchy.py +3 -6
  97. infrahub/core/validators/query.py +1 -3
  98. infrahub/core/validators/relationship/count.py +6 -12
  99. infrahub/core/validators/relationship/optional.py +2 -4
  100. infrahub/core/validators/relationship/peer.py +3 -8
  101. infrahub/core/validators/tasks.py +1 -1
  102. infrahub/core/validators/uniqueness/query.py +12 -9
  103. infrahub/database/__init__.py +1 -3
  104. infrahub/events/group_action.py +1 -0
  105. infrahub/graphql/analyzer.py +139 -18
  106. infrahub/graphql/app.py +1 -1
  107. infrahub/graphql/loaders/node.py +1 -1
  108. infrahub/graphql/loaders/peers.py +1 -1
  109. infrahub/graphql/manager.py +4 -0
  110. infrahub/graphql/mutations/action.py +164 -0
  111. infrahub/graphql/mutations/convert_object_type.py +62 -0
  112. infrahub/graphql/mutations/main.py +24 -175
  113. infrahub/graphql/mutations/proposed_change.py +21 -18
  114. infrahub/graphql/queries/convert_object_type_mapping.py +36 -0
  115. infrahub/graphql/queries/diff/tree.py +2 -1
  116. infrahub/graphql/queries/relationship.py +1 -1
  117. infrahub/graphql/resolvers/many_relationship.py +4 -4
  118. infrahub/graphql/resolvers/resolver.py +4 -4
  119. infrahub/graphql/resolvers/single_relationship.py +2 -2
  120. infrahub/graphql/schema.py +6 -0
  121. infrahub/graphql/subscription/graphql_query.py +2 -2
  122. infrahub/graphql/types/branch.py +1 -1
  123. infrahub/menu/menu.py +31 -0
  124. infrahub/message_bus/messages/__init__.py +0 -10
  125. infrahub/message_bus/operations/__init__.py +0 -8
  126. infrahub/message_bus/operations/refresh/registry.py +1 -1
  127. infrahub/patch/queries/consolidate_duplicated_nodes.py +3 -6
  128. infrahub/patch/queries/delete_duplicated_edges.py +5 -10
  129. infrahub/prefect_server/models.py +1 -19
  130. infrahub/proposed_change/models.py +68 -3
  131. infrahub/proposed_change/tasks.py +907 -30
  132. infrahub/task_manager/models.py +10 -6
  133. infrahub/telemetry/database.py +1 -1
  134. infrahub/telemetry/tasks.py +1 -1
  135. infrahub/trigger/catalogue.py +2 -0
  136. infrahub/trigger/models.py +29 -3
  137. infrahub/trigger/setup.py +51 -15
  138. infrahub/trigger/tasks.py +4 -5
  139. infrahub/types.py +1 -1
  140. infrahub/webhook/models.py +2 -1
  141. infrahub/workflows/catalogue.py +85 -0
  142. infrahub/workflows/initialization.py +1 -3
  143. infrahub_sdk/timestamp.py +2 -2
  144. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/METADATA +4 -4
  145. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/RECORD +153 -146
  146. infrahub_testcontainers/container.py +0 -1
  147. infrahub_testcontainers/docker-compose.test.yml +4 -4
  148. infrahub_testcontainers/helpers.py +8 -2
  149. infrahub_testcontainers/performance_test.py +6 -3
  150. infrahub/message_bus/messages/check_generator_run.py +0 -26
  151. infrahub/message_bus/messages/finalize_validator_execution.py +0 -15
  152. infrahub/message_bus/messages/proposed_change/base_with_diff.py +0 -16
  153. infrahub/message_bus/messages/proposed_change/request_proposedchange_refreshartifacts.py +0 -11
  154. infrahub/message_bus/messages/request_generatordefinition_check.py +0 -20
  155. infrahub/message_bus/messages/request_proposedchange_pipeline.py +0 -23
  156. infrahub/message_bus/operations/check/__init__.py +0 -3
  157. infrahub/message_bus/operations/check/generator.py +0 -156
  158. infrahub/message_bus/operations/finalize/__init__.py +0 -3
  159. infrahub/message_bus/operations/finalize/validator.py +0 -133
  160. infrahub/message_bus/operations/requests/__init__.py +0 -9
  161. infrahub/message_bus/operations/requests/generator_definition.py +0 -140
  162. infrahub/message_bus/operations/requests/proposed_change.py +0 -629
  163. /infrahub/{message_bus/messages/proposed_change → actions}/__init__.py +0 -0
  164. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/LICENSE.txt +0 -0
  165. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/WHEEL +0 -0
  166. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/entry_points.txt +0 -0
@@ -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,
@@ -20,7 +20,7 @@ log = get_logger()
20
20
  async def update_diff(model: RequestDiffUpdate, service: InfrahubServices) -> None:
21
21
  await add_tags(branches=[model.branch_name])
22
22
 
23
- async with service.database.start_session() as db:
23
+ async with service.database.start_session(read_only=False) as db:
24
24
  component_registry = get_component_registry()
25
25
  base_branch = await registry.get_branch(db=db, branch=registry.default_branch)
26
26
  diff_branch = await registry.get_branch(db=db, branch=model.branch_name)
@@ -40,7 +40,7 @@ async def update_diff(model: RequestDiffUpdate, service: InfrahubServices) -> No
40
40
  async def refresh_diff(branch_name: str, diff_id: str, service: InfrahubServices) -> None:
41
41
  await add_tags(branches=[branch_name])
42
42
 
43
- async with service.database.start_session() as db:
43
+ async with service.database.start_session(read_only=False) as db:
44
44
  component_registry = get_component_registry()
45
45
  base_branch = await registry.get_branch(db=db, branch=registry.default_branch)
46
46
  diff_branch = await registry.get_branch(db=db, branch=branch_name)
@@ -53,7 +53,7 @@ async def refresh_diff(branch_name: str, diff_id: str, service: InfrahubServices
53
53
  async def refresh_diff_all(branch_name: str, context: InfrahubContext, service: InfrahubServices) -> None:
54
54
  await add_tags(branches=[branch_name])
55
55
 
56
- async with service.database.start_session() as db:
56
+ async with service.database.start_session(read_only=True) as db:
57
57
  component_registry = get_component_registry()
58
58
  default_branch = registry.get_branch_from_registry()
59
59
  diff_repository = await component_registry.get_component(DiffRepository, db=db, branch=default_branch)
@@ -1 +1 @@
1
- GRAPH_VERSION = 26
1
+ GRAPH_VERSION = 28
infrahub/core/manager.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from copy import copy
3
4
  from functools import reduce
4
5
  from typing import TYPE_CHECKING, Any, Iterable, Literal, TypeVar, overload
5
6
 
@@ -1339,22 +1340,24 @@ class NodeManager:
1339
1340
  nodes: list[Node],
1340
1341
  branch: Branch | str | None = None,
1341
1342
  at: Timestamp | str | None = None,
1343
+ cascade_delete: bool = True,
1342
1344
  ) -> list[Node]:
1343
1345
  """Returns list of deleted nodes because of cascading deletes"""
1344
1346
  branch = await registry.get_branch(branch=branch, db=db)
1345
- node_delete_validator = NodeDeleteValidator(db=db, branch=branch)
1346
- ids_to_delete = await node_delete_validator.get_ids_to_delete(nodes=nodes, at=at)
1347
- node_ids = {node.get_id() for node in nodes}
1348
- missing_ids_to_delete = ids_to_delete - node_ids
1349
- if missing_ids_to_delete:
1350
- node_map = await cls.get_many(db=db, ids=list(missing_ids_to_delete), branch=branch, at=at)
1351
- nodes += list(node_map.values())
1352
- deleted_nodes = []
1353
- for node in nodes:
1347
+ nodes_to_delete = copy(nodes)
1348
+ if cascade_delete:
1349
+ node_delete_validator = NodeDeleteValidator(db=db, branch=branch)
1350
+ ids_to_delete = await node_delete_validator.get_ids_to_delete(nodes=nodes, at=at)
1351
+ node_ids = {node.get_id() for node in nodes}
1352
+ missing_ids_to_delete = ids_to_delete - node_ids
1353
+ if missing_ids_to_delete:
1354
+ node_map = await cls.get_many(db=db, ids=list(missing_ids_to_delete), branch=branch, at=at)
1355
+ nodes_to_delete += list(node_map.values())
1356
+
1357
+ for node in nodes_to_delete:
1354
1358
  await node.delete(db=db, at=at)
1355
- deleted_nodes.append(node)
1356
1359
 
1357
- return deleted_nodes
1360
+ return nodes_to_delete
1358
1361
 
1359
1362
 
1360
1363
  def _get_cardinality_one_identifiers_by_kind(
@@ -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
 
@@ -26,8 +26,7 @@ class Migration003Query01(Query):
26
26
  query = """
27
27
  MATCH path = (av2:AttributeValue)-[:HAS_VALUE]-(:Attribute {name: "optional"})-[:HAS_ATTRIBUTE]-(n:SchemaRelationship)-[:HAS_ATTRIBUTE]-(:Attribute {name: "kind"})-[:HAS_VALUE]-(av1:AttributeValue)
28
28
  WHERE av1.value = "Parent" AND av2.value = true AND all(r IN relationships(path) WHERE ( %(filters)s ))
29
- CALL {
30
- WITH n
29
+ CALL (n) {
31
30
  MATCH path22 = (av22:AttributeValue)-[r22:HAS_VALUE]-(a2:Attribute {name: "optional"})-[:HAS_ATTRIBUTE]-(n:SchemaRelationship)-[:HAS_ATTRIBUTE]-(:Attribute {name:"kind"})-[:HAS_VALUE]-(av11:AttributeValue)
32
31
  WHERE av11.value = "Parent" AND av22.value = true AND all(r IN relationships(path22) WHERE ( %(filters)s ))
33
32
  RETURN av22 as av2_sub, r22, a2, path22 as path2
@@ -98,8 +98,7 @@ class Migration013ConvertCoreRepositoryWithCred(Query):
98
98
  // --------------------------------
99
99
  MATCH (git_repo)-[:HAS_ATTRIBUTE]-(git_attr_name:Attribute)-[:HAS_VALUE]->(git_name_value:AttributeValue)
100
100
  WHERE git_attr_name.name = "name"
101
- CALL {
102
- WITH git_repo
101
+ CALL (git_repo) {
103
102
  MATCH path1 = (git_repo)-[r1:HAS_ATTRIBUTE]-(git_attr_name2:Attribute)-[r2:HAS_VALUE]->(git_name_value2:AttributeValue)
104
103
  WHERE git_attr_name2.name = "name"
105
104
  AND all(r IN relationships(path1) WHERE %(filters)s)
@@ -204,8 +203,7 @@ class Migration013ConvertCoreRepositoryWithoutCred(Query):
204
203
  WHERE a.name in ["username", "password"]
205
204
  AND av.value = "NULL"
206
205
  AND all(r IN relationships(path) WHERE %(filters)s AND r.status = "active")
207
- CALL {
208
- WITH node
206
+ CALL (node) {
209
207
  MATCH (root:Root)<-[r:IS_PART_OF]-(node)
210
208
  WHERE %(filters)s
211
209
  RETURN node as n1, r as r1
@@ -97,8 +97,7 @@ class DeleteNodesRelsQuery(Query):
97
97
  // - on deleted edge branch
98
98
  // - or on any branch and deleted node is agnostic
99
99
  // - or deleted node is aware and rel is agnostic
100
- CALL {
101
- WITH rel, deleted_edge
100
+ CALL (rel, deleted_edge) {
102
101
  OPTIONAL MATCH (rel)-[peer_active_edge {status: "active"}]-(peer_1)
103
102
  WHERE (peer_active_edge.branch = deleted_edge.branch OR (rel.branch_support <> $branch_agnostic AND deleted_edge.branch = $global_branch))
104
103
  AND peer_active_edge.to IS NULL
@@ -160,62 +159,52 @@ class DeleteNodesRelsQuery(Query):
160
159
  // Below CALL subqueries are called once for each rel-peer_2 pair for which we want to create a deleted edge.
161
160
  // Note that with current infrahub relationships edges design, only one of this CALL should be matched per pair.
162
161
 
163
- CALL {
164
- WITH rel, peer_2, branch, branch_level, deleted_time
162
+ CALL (rel, peer_2, branch, branch_level, deleted_time) {
165
163
  MATCH (rel)-[:IS_RELATED]->(peer_2)
166
164
  MERGE (rel)-[:IS_RELATED {status: "deleted", branch: branch, branch_level: branch_level, from: deleted_time}]->(peer_2)
167
165
  }
168
166
 
169
- CALL {
170
- WITH rel, peer_2, branch, branch_level, deleted_time
167
+ CALL (rel, peer_2, branch, branch_level, deleted_time) {
171
168
  MATCH (rel)-[:IS_PROTECTED]->(peer_2)
172
169
  MERGE (rel)-[:IS_PROTECTED {status: "deleted", branch: branch, branch_level: branch_level, from: deleted_time}]->(peer_2)
173
170
  }
174
171
 
175
- CALL {
176
- WITH rel, peer_2, branch, branch_level, deleted_time
172
+ CALL (rel, peer_2, branch, branch_level, deleted_time) {
177
173
  MATCH (rel)-[:IS_VISIBLE]->(peer_2)
178
174
  MERGE (rel)-[:IS_VISIBLE {status: "deleted", branch: branch, branch_level: branch_level, from: deleted_time}]->(peer_2)
179
175
  }
180
176
 
181
- CALL {
182
- WITH rel, peer_2, branch, branch_level, deleted_time
177
+ CALL (rel, peer_2, branch, branch_level, deleted_time) {
183
178
  MATCH (rel)-[:HAS_OWNER]->(peer_2)
184
179
  MERGE (rel)-[:HAS_OWNER {status: "deleted", branch: branch, branch_level: branch_level, from: deleted_time}]->(peer_2)
185
180
  }
186
181
 
187
- CALL {
188
- WITH rel, peer_2, branch, branch_level, deleted_time
182
+ CALL (rel, peer_2, branch, branch_level, deleted_time) {
189
183
  MATCH (rel)-[:HAS_SOURCE]->(peer_2)
190
184
  MERGE (rel)-[:HAS_SOURCE {status: "deleted", branch: branch, branch_level: branch_level, from: deleted_time}]->(peer_2)
191
185
  }
192
186
 
193
- CALL {
194
- WITH rel, peer_2, branch, branch_level, deleted_time
187
+ CALL (rel, peer_2, branch, branch_level, deleted_time) {
195
188
  MATCH (rel)<-[:IS_RELATED]-(peer_2)
196
189
  MERGE (rel)<-[:IS_RELATED {status: "deleted", branch: branch, branch_level: branch_level, from: deleted_time}]-(peer_2)
197
190
  }
198
191
 
199
- CALL {
200
- WITH rel, peer_2, branch, branch_level, deleted_time
192
+ CALL (rel, peer_2, branch, branch_level, deleted_time) {
201
193
  MATCH (rel)<-[:IS_PROTECTED]-(peer_2)
202
194
  MERGE (rel)<-[:IS_PROTECTED {status: "deleted", branch: branch, branch_level: branch_level, from: deleted_time}]-(peer_2)
203
195
  }
204
196
 
205
- CALL {
206
- WITH rel, peer_2, branch, branch_level, deleted_time
197
+ CALL (rel, peer_2, branch, branch_level, deleted_time) {
207
198
  MATCH (rel)<-[:IS_VISIBLE]-(peer_2)
208
199
  MERGE (rel)<-[:IS_VISIBLE {status: "deleted", branch: branch, branch_level: branch_level, from: deleted_time}]-(peer_2)
209
200
  }
210
201
 
211
- CALL {
212
- WITH rel, peer_2, branch, branch_level, deleted_time
202
+ CALL (rel, peer_2, branch, branch_level, deleted_time) {
213
203
  MATCH (rel)<-[:HAS_OWNER]-(peer_2)
214
204
  MERGE (rel)<-[:HAS_OWNER {status: "deleted", branch: branch, branch_level: branch_level, from: deleted_time}]-(peer_2)
215
205
  }
216
206
 
217
- CALL {
218
- WITH rel, peer_2, branch, branch_level, deleted_time
207
+ CALL (rel, peer_2, branch, branch_level, deleted_time) {
219
208
  MATCH (rel)<-[:HAS_SOURCE]-(peer_2)
220
209
  MERGE (rel)<-[:HAS_SOURCE {status: "deleted", branch: branch, branch_level: branch_level, from: deleted_time}]-(peer_2)
221
210
  }
@@ -33,8 +33,7 @@ WHERE num_duplicate_edges > 1
33
33
  // -------------------
34
34
  WITH DISTINCT a, branch, branch_level, status, from, to, attr_val, attr_default
35
35
  WITH attr_val, attr_default, collect([a, branch, branch_level, status, from, to]) AS details_list
36
- CALL {
37
- WITH attr_val, attr_default
36
+ CALL (attr_val, attr_default) {
38
37
  MATCH (av:AttributeValue {value: attr_val, is_default: attr_default})
39
38
  RETURN av AS the_one_av
40
39
  ORDER by %(id_func)s(av) ASC
@@ -53,11 +52,10 @@ WITH a, branch, status, from, to, attr_val, attr_default, %(id_func)s(fresh_e) A
53
52
  // -------------------
54
53
  // get the identical edges for a given set of Attribute node, edge properties, AttributeValue.value
55
54
  // -------------------
56
- CALL {
55
+ CALL (a, branch, status, from, to, attr_val, attr_default, e_id_to_keep) {
57
56
  // -------------------
58
57
  // delete the duplicate edges a given set of Attribute node, edge properties, AttributeValue.value
59
58
  // -------------------
60
- WITH a, branch, status, from, to, attr_val, attr_default, e_id_to_keep
61
59
  MATCH (a)-[e:HAS_VALUE]->(av:AttributeValue {value: attr_val, is_default: attr_default})
62
60
  WHERE %(id_func)s(e) <> e_id_to_keep
63
61
  AND e.branch = branch AND e.status = status AND e.from = from
@@ -99,8 +97,7 @@ WITH DISTINCT a, branch, branch_level, status, from, to, b
99
97
  CREATE (a)-[fresh_e:%(edge_type)s {branch: branch, branch_level: branch_level, status: status, from: from}]->(b)
100
98
  SET fresh_e.to = to
101
99
  WITH a, branch, status, from, to, b, %(id_func)s(fresh_e) AS e_id_to_keep
102
- CALL {
103
- WITH a, branch, status, from, to, b, e_id_to_keep
100
+ CALL (a, branch, status, from, to, b, e_id_to_keep) {
104
101
  MATCH (a)-[e:%(edge_type)s]->(b)
105
102
  WHERE %(id_func)s(e) <> e_id_to_keep
106
103
  AND e.branch = branch AND e.status = status AND e.from = from
@@ -24,8 +24,7 @@ class SetMissingHierarchyQuery(Query):
24
24
  WITH r.default_branch AS default_branch
25
25
  MATCH (n:Node)-[main_e:IS_RELATED {branch: default_branch}]-(rel:Relationship)
26
26
  WHERE main_e.hierarchy IS NULL
27
- CALL {
28
- WITH n, main_e, rel
27
+ CALL (n, main_e, rel) {
29
28
  MATCH (n)-[branch_e:IS_RELATED]-(rel)
30
29
  WHERE branch_e.hierarchy IS NOT NULL
31
30
  AND branch_e.branch <> main_e.branch
@@ -39,8 +39,7 @@ class BackfillMissingHierarchyQuery(Query):
39
39
  MATCH (rel:Relationship {name: "parent__child"})-[e:IS_RELATED]-(n:Node)
40
40
  WHERE e.hierarchy IS NULL
41
41
  WITH DISTINCT rel, n, default_branch
42
- CALL {
43
- WITH rel, n, default_branch
42
+ CALL (rel, n, default_branch) {
44
43
  MATCH (rel)-[e:IS_RELATED {branch: default_branch}]-(n)
45
44
  RETURN e
46
45
  ORDER BY e.from DESC
@@ -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()
@@ -61,8 +61,7 @@ class AttributeAddQuery(Query):
61
61
  MERGE (is_visible_value:Boolean { value: $is_visible_default })
62
62
  WITH av, is_protected_value, is_visible_value
63
63
  MATCH p = (n:%(node_kind)s)
64
- CALL {
65
- WITH n
64
+ CALL (n) {
66
65
  MATCH (root:Root)<-[r1:IS_PART_OF]-(n)
67
66
  OPTIONAL MATCH (n)-[r2:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name })
68
67
  WHERE all(r in [r1, r2] WHERE (%(branch_filter)s))