infrahub-server 1.1.6__py3-none-any.whl → 1.1.8__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 (97) hide show
  1. infrahub/core/attribute.py +4 -1
  2. infrahub/core/branch/tasks.py +7 -4
  3. infrahub/core/diff/combiner.py +11 -7
  4. infrahub/core/diff/coordinator.py +49 -70
  5. infrahub/core/diff/data_check_synchronizer.py +86 -7
  6. infrahub/core/diff/enricher/aggregated.py +3 -3
  7. infrahub/core/diff/enricher/cardinality_one.py +6 -6
  8. infrahub/core/diff/enricher/hierarchy.py +17 -4
  9. infrahub/core/diff/enricher/labels.py +18 -3
  10. infrahub/core/diff/enricher/path_identifier.py +7 -8
  11. infrahub/core/diff/merger/merger.py +5 -3
  12. infrahub/core/diff/model/path.py +66 -25
  13. infrahub/core/diff/parent_node_adder.py +78 -0
  14. infrahub/core/diff/payload_builder.py +13 -2
  15. infrahub/core/diff/query/all_conflicts.py +5 -2
  16. infrahub/core/diff/query/diff_get.py +2 -1
  17. infrahub/core/diff/query/field_specifiers.py +2 -0
  18. infrahub/core/diff/query/field_summary.py +2 -1
  19. infrahub/core/diff/query/filters.py +12 -1
  20. infrahub/core/diff/query/has_conflicts_query.py +5 -2
  21. infrahub/core/diff/query/{drop_tracking_id.py → merge_tracking_id.py} +3 -3
  22. infrahub/core/diff/query/roots_metadata.py +8 -1
  23. infrahub/core/diff/query/save.py +230 -139
  24. infrahub/core/diff/query/summary_counts_enricher.py +267 -0
  25. infrahub/core/diff/query/time_range_query.py +2 -1
  26. infrahub/core/diff/query_parser.py +49 -24
  27. infrahub/core/diff/repository/deserializer.py +31 -27
  28. infrahub/core/diff/repository/repository.py +215 -41
  29. infrahub/core/diff/tasks.py +4 -4
  30. infrahub/core/graph/__init__.py +1 -1
  31. infrahub/core/graph/index.py +3 -0
  32. infrahub/core/migrations/graph/__init__.py +4 -0
  33. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +256 -0
  34. infrahub/core/migrations/graph/m020_duplicate_edges.py +160 -0
  35. infrahub/core/migrations/query/node_duplicate.py +38 -18
  36. infrahub/core/migrations/schema/node_remove.py +26 -12
  37. infrahub/core/migrations/shared.py +10 -8
  38. infrahub/core/node/__init__.py +19 -9
  39. infrahub/core/node/constraints/grouped_uniqueness.py +25 -5
  40. infrahub/core/node/ipam.py +6 -1
  41. infrahub/core/node/permissions.py +4 -0
  42. infrahub/core/query/attribute.py +2 -0
  43. infrahub/core/query/diff.py +41 -3
  44. infrahub/core/query/node.py +74 -21
  45. infrahub/core/query/relationship.py +107 -17
  46. infrahub/core/query/resource_manager.py +5 -1
  47. infrahub/core/relationship/model.py +8 -12
  48. infrahub/core/schema/definitions/core.py +1 -0
  49. infrahub/core/utils.py +1 -0
  50. infrahub/core/validators/uniqueness/query.py +20 -17
  51. infrahub/database/__init__.py +14 -0
  52. infrahub/dependencies/builder/constraint/grouped/node_runner.py +0 -2
  53. infrahub/dependencies/builder/diff/coordinator.py +0 -2
  54. infrahub/dependencies/builder/diff/deserializer.py +3 -1
  55. infrahub/dependencies/builder/diff/enricher/hierarchy.py +3 -1
  56. infrahub/dependencies/builder/diff/parent_node_adder.py +8 -0
  57. infrahub/graphql/mutations/computed_attribute.py +3 -1
  58. infrahub/graphql/mutations/diff.py +41 -10
  59. infrahub/graphql/mutations/main.py +11 -6
  60. infrahub/graphql/mutations/relationship.py +29 -1
  61. infrahub/graphql/mutations/resource_manager.py +3 -3
  62. infrahub/graphql/mutations/tasks.py +6 -3
  63. infrahub/graphql/queries/resource_manager.py +7 -3
  64. infrahub/permissions/__init__.py +2 -1
  65. infrahub/permissions/types.py +26 -0
  66. infrahub_sdk/client.py +10 -2
  67. infrahub_sdk/config.py +3 -0
  68. infrahub_sdk/ctl/check.py +3 -3
  69. infrahub_sdk/ctl/cli_commands.py +16 -11
  70. infrahub_sdk/ctl/exceptions.py +0 -6
  71. infrahub_sdk/ctl/exporter.py +1 -1
  72. infrahub_sdk/ctl/generator.py +5 -5
  73. infrahub_sdk/ctl/importer.py +3 -2
  74. infrahub_sdk/ctl/menu.py +1 -1
  75. infrahub_sdk/ctl/object.py +1 -1
  76. infrahub_sdk/ctl/repository.py +23 -15
  77. infrahub_sdk/ctl/schema.py +2 -2
  78. infrahub_sdk/ctl/utils.py +4 -3
  79. infrahub_sdk/ctl/validate.py +2 -1
  80. infrahub_sdk/exceptions.py +12 -0
  81. infrahub_sdk/generator.py +3 -0
  82. infrahub_sdk/node.py +7 -4
  83. infrahub_sdk/testing/schemas/animal.py +9 -0
  84. infrahub_sdk/utils.py +11 -1
  85. infrahub_sdk/yaml.py +2 -3
  86. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.8.dist-info}/METADATA +41 -7
  87. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.8.dist-info}/RECORD +94 -91
  88. infrahub_testcontainers/container.py +12 -3
  89. infrahub_testcontainers/docker-compose.test.yml +22 -3
  90. infrahub_testcontainers/haproxy.cfg +43 -0
  91. infrahub_testcontainers/helpers.py +85 -1
  92. infrahub/core/diff/enricher/summary_counts.py +0 -105
  93. infrahub/dependencies/builder/diff/enricher/summary_counts.py +0 -8
  94. infrahub_sdk/ctl/_file.py +0 -13
  95. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.8.dist-info}/LICENSE.txt +0 -0
  96. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.8.dist-info}/WHEEL +0 -0
  97. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.8.dist-info}/entry_points.txt +0 -0
@@ -7,7 +7,13 @@ from infrahub_sdk.utils import is_valid_uuid
7
7
  from infrahub_sdk.uuidt import UUIDT
8
8
 
9
9
  from infrahub.core import registry
10
- from infrahub.core.constants import BranchSupportType, ComputedAttributeKind, InfrahubKind, RelationshipCardinality
10
+ from infrahub.core.constants import (
11
+ GLOBAL_BRANCH_NAME,
12
+ BranchSupportType,
13
+ ComputedAttributeKind,
14
+ InfrahubKind,
15
+ RelationshipCardinality,
16
+ )
11
17
  from infrahub.core.constants.schema import SchemaElementPathType
12
18
  from infrahub.core.protocols import CoreNumberPool
13
19
  from infrahub.core.query.node import NodeCheckIDQuery, NodeCreateAllQuery, NodeDeleteQuery, NodeGetListQuery
@@ -19,6 +25,7 @@ from infrahub.types import ATTRIBUTE_TYPES
19
25
 
20
26
  from ...graphql.constants import KIND_GRAPHQL_FIELD_NAME
21
27
  from ...graphql.models import OrderModel
28
+ from ..query.relationship import RelationshipDeleteAllQuery
22
29
  from ..relationship import RelationshipManager
23
30
  from ..utils import update_relationships_to
24
31
  from .base import BaseNode, BaseNodeMeta, BaseNodeOptions
@@ -599,15 +606,13 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
599
606
  attr: BaseAttribute = getattr(self, name)
600
607
  await attr.delete(at=delete_at, db=db)
601
608
 
602
- # Go over the list of relationships and update them one by one
603
- for name in self._relationships:
604
- rel: RelationshipManager = getattr(self, name)
605
- await rel.delete(at=delete_at, db=db)
606
-
607
- # Need to check if there are some unidirectional relationship as well
608
- # For example, if we delete a tag, we must check the permissions and update all the relationships pointing at it
609
609
  branch = self.get_branch_based_on_support_type()
610
610
 
611
+ delete_query = await RelationshipDeleteAllQuery.init(
612
+ db=db, node_id=self.get_id(), branch=branch, at=delete_at, branch_agnostic=branch.name == GLOBAL_BRANCH_NAME
613
+ )
614
+ await delete_query.execute(db=db)
615
+
611
616
  # Update the relationship to the branch itself
612
617
  query = await NodeGetListQuery.init(
613
618
  db=db,
@@ -633,6 +638,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
633
638
  related_node_ids: set | None = None,
634
639
  filter_sensitive: bool = False,
635
640
  permissions: dict | None = None,
641
+ include_properties: bool = True,
636
642
  ) -> dict:
637
643
  """Generate GraphQL Payload for all attributes
638
644
 
@@ -686,10 +692,14 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
686
692
  related_node_ids=related_node_ids,
687
693
  filter_sensitive=filter_sensitive,
688
694
  permissions=permissions,
695
+ include_properties=include_properties,
689
696
  )
690
697
  else:
691
698
  response[field_name] = await field.to_graphql(
692
- db=db, filter_sensitive=filter_sensitive, permissions=permissions
699
+ db=db,
700
+ filter_sensitive=filter_sensitive,
701
+ permissions=permissions,
702
+ include_properties=include_properties,
693
703
  )
694
704
 
695
705
  for relationship_schema in self.get_schema().relationships:
@@ -35,7 +35,7 @@ class NodeGroupedUniquenessConstraint(NodeConstraintInterface):
35
35
  self.branch = branch
36
36
  self.schema_branch = registry.schema.get_schema_branch(branch.name)
37
37
 
38
- def _build_query_request(
38
+ async def _build_query_request(
39
39
  self,
40
40
  updated_node: Node,
41
41
  node_schema: MainSchemaTypes,
@@ -51,9 +51,16 @@ class NodeGroupedUniquenessConstraint(NodeConstraintInterface):
51
51
  if attribute_path.related_schema and attribute_path.relationship_schema:
52
52
  if filters and attribute_path.relationship_schema.name in filters:
53
53
  include_in_query = True
54
+
55
+ relationship_manager: RelationshipManager = getattr(
56
+ updated_node, attribute_path.relationship_schema.name
57
+ )
58
+ related_node = await relationship_manager.get_peer(db=self.db)
59
+ related_node_id = related_node.get_id() if related_node else None
54
60
  query_relationship_paths.add(
55
61
  QueryRelationshipAttributePath(
56
62
  identifier=attribute_path.relationship_schema.get_identifier(),
63
+ value=related_node_id,
57
64
  )
58
65
  )
59
66
  continue
@@ -158,7 +165,7 @@ class NodeGroupedUniquenessConstraint(NodeConstraintInterface):
158
165
  ) -> None:
159
166
  schema_branch = self.db.schema.get_schema_branch(name=self.branch.name)
160
167
  path_groups = node_schema.get_unique_constraint_schema_attribute_paths(schema_branch=schema_branch)
161
- query_request = self._build_query_request(
168
+ query_request = await self._build_query_request(
162
169
  updated_node=node, node_schema=node_schema, path_groups=path_groups, filters=filters
163
170
  )
164
171
  if not query_request:
@@ -170,12 +177,25 @@ class NodeGroupedUniquenessConstraint(NodeConstraintInterface):
170
177
  await self._check_results(updated_node=node, path_groups=path_groups, query_results=query.get_results())
171
178
 
172
179
  async def check(self, node: Node, at: Optional[Timestamp] = None, filters: Optional[list[str]] = None) -> None:
180
+ def _frozen_constraints(schema: MainSchemaTypes) -> frozenset[frozenset[str]]:
181
+ if not schema.uniqueness_constraints:
182
+ return frozenset()
183
+ return frozenset(frozenset(uc) for uc in schema.uniqueness_constraints)
184
+
173
185
  node_schema = node.get_schema()
174
- schemas_to_check: list[MainSchemaTypes] = [node_schema]
186
+ include_node_schema = True
187
+ frozen_node_constraints = _frozen_constraints(node_schema)
188
+ schemas_to_check: list[MainSchemaTypes] = []
175
189
  if node_schema.inherit_from:
176
190
  for parent_schema_name in node_schema.inherit_from:
177
191
  parent_schema = self.schema_branch.get(name=parent_schema_name, duplicate=False)
178
- if parent_schema.uniqueness_constraints:
179
- schemas_to_check.append(parent_schema)
192
+ if not parent_schema.uniqueness_constraints:
193
+ continue
194
+ schemas_to_check.append(parent_schema)
195
+ frozen_parent_constraints = _frozen_constraints(parent_schema)
196
+ if frozen_node_constraints <= frozen_parent_constraints:
197
+ include_node_schema = False
198
+ if include_node_schema:
199
+ schemas_to_check.append(node_schema)
180
200
  for schema in schemas_to_check:
181
201
  await self._check_one_schema(node=node, node_schema=schema, at=at, filters=filters)
@@ -20,9 +20,14 @@ class BuiltinIPPrefix(Node):
20
20
  related_node_ids: Optional[set] = None,
21
21
  filter_sensitive: bool = False,
22
22
  permissions: Optional[dict] = None,
23
+ include_properties: bool = True,
23
24
  ) -> dict:
24
25
  response = await super().to_graphql(
25
- db, fields=fields, related_node_ids=related_node_ids, filter_sensitive=filter_sensitive
26
+ db,
27
+ fields=fields,
28
+ related_node_ids=related_node_ids,
29
+ filter_sensitive=filter_sensitive,
30
+ include_properties=include_properties,
26
31
  )
27
32
 
28
33
  if fields:
@@ -18,6 +18,7 @@ class CoreGlobalPermission(Node):
18
18
  related_node_ids: Optional[set] = None,
19
19
  filter_sensitive: bool = False,
20
20
  permissions: Optional[dict] = None,
21
+ include_properties: bool = True,
21
22
  ) -> dict:
22
23
  response = await super().to_graphql(
23
24
  db,
@@ -25,6 +26,7 @@ class CoreGlobalPermission(Node):
25
26
  related_node_ids=related_node_ids,
26
27
  filter_sensitive=filter_sensitive,
27
28
  permissions=permissions,
29
+ include_properties=include_properties,
28
30
  )
29
31
 
30
32
  if fields:
@@ -43,6 +45,7 @@ class CoreObjectPermission(Node):
43
45
  related_node_ids: Optional[set] = None,
44
46
  filter_sensitive: bool = False,
45
47
  permissions: Optional[dict] = None,
48
+ include_properties: bool = True,
46
49
  ) -> dict:
47
50
  response = await super().to_graphql(
48
51
  db,
@@ -50,6 +53,7 @@ class CoreObjectPermission(Node):
50
53
  related_node_ids=related_node_ids,
51
54
  filter_sensitive=filter_sensitive,
52
55
  permissions=permissions,
56
+ include_properties=include_properties,
53
57
  )
54
58
 
55
59
  if fields:
@@ -66,6 +66,8 @@ class AttributeUpdateValueQuery(AttributeQuery):
66
66
  query = """
67
67
  MATCH (a:Attribute { uuid: $attr_uuid })
68
68
  MERGE (av:%(labels)s { %(props)s } )
69
+ WITH av, a
70
+ LIMIT 1
69
71
  CREATE (a)-[r:%(rel_label)s { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(av)
70
72
  """ % {"rel_label": self.attr._rel_to_value_label, "labels": ":".join(labels), "props": ", ".join(prop_list)}
71
73
 
@@ -137,6 +137,14 @@ CALL {
137
137
  r in relationships(latest_base_path)
138
138
  WHERE r.from < $branch_from_time
139
139
  )
140
+ // ------------------------
141
+ // special handling for nodes that had their kind updated,
142
+ // the migration leaves two nodes with the same UUID linked to the same Relationship
143
+ // ------------------------
144
+ AND (
145
+ n.uuid IS NULL OR base_prop.uuid IS NULL OR n.uuid <> base_prop.uuid
146
+ OR type(base_r_node) <> "IS_RELATED" OR type(base_r_prop) <> "IS_RELATED"
147
+ )
140
148
  WITH latest_base_path, base_r_root, base_r_node, base_r_prop
141
149
  ORDER BY base_r_prop.from DESC, base_r_node.from DESC, base_r_root.from DESC
142
150
  LIMIT 1
@@ -146,7 +154,7 @@ CALL {
146
154
  relationship_peer_side_query = """
147
155
  WITH diff_path, latest_base_path, has_more_data
148
156
  UNWIND [diff_path, latest_base_path] AS penultimate_path
149
- WITH penultimate_path, has_more_data
157
+ WITH DISTINCT penultimate_path, has_more_data
150
158
  CALL {
151
159
  WITH penultimate_path
152
160
  WITH penultimate_path, nodes(penultimate_path) AS d_nodes, relationships(penultimate_path) AS d_rels
@@ -175,8 +183,13 @@ CALL {
175
183
  OR peer_r_node.to IS NULL
176
184
  OR peer_r_node.to >= r_peer.from
177
185
  )
186
+ // ------------------------
187
+ // special handling for nodes that had their kind updated,
188
+ // the migration leaves two nodes with the same UUID linked to the same Relationship
189
+ // ------------------------
190
+ AND (n.uuid IS NULL OR peer.uuid IS NULL OR n.uuid <> peer.uuid)
178
191
  WITH peer_path, r_peer, r_prop
179
- ORDER BY r_peer.branch = r_prop.branch DESC, r_peer.from DESC
192
+ ORDER BY r_peer.branch = r_prop.branch DESC, r_peer.status = r_prop.status DESC, r_peer.from DESC, r_peer.status ASC
180
193
  LIMIT 1
181
194
  RETURN peer_path
182
195
  }
@@ -309,6 +322,14 @@ CALL {
309
322
  AND [%(id_func)s(p), type(r_node)] <> [%(id_func)s(prop), type(r_prop)]
310
323
  AND top_diff_rel.status = r_node.status
311
324
  AND top_diff_rel.status = r_prop.status
325
+ // ------------------------
326
+ // special handling for nodes that had their kind updated,
327
+ // the migration leaves two nodes with the same UUID linked to the same Relationship
328
+ // ------------------------
329
+ AND (
330
+ p.uuid IS NULL OR prop.uuid IS NULL OR p.uuid <> prop.uuid
331
+ OR type(r_node) <> "IS_RELATED" OR type(r_prop) <> "IS_RELATED"
332
+ )
312
333
  WITH path, p, node, prop, r_prop, r_node, type(r_node) AS rel_type, row_from_time
313
334
  // -------------------------------------
314
335
  // Exclude attributes/relationships added then removed on branch within timeframe
@@ -483,17 +504,26 @@ CALL {
483
504
  AND [%(id_func)s(p), type(mid_diff_rel)] <> [%(id_func)s(prop), type(r_prop)]
484
505
  // exclude paths where an active edge is below a deleted edge
485
506
  AND (mid_diff_rel.status = "active" OR r_prop.status = "deleted")
507
+ // ------------------------
508
+ // special handling for nodes that had their kind updated,
509
+ // the migration leaves two nodes with the same UUID linked to the same Relationship
510
+ // ------------------------
511
+ AND (
512
+ p.uuid IS NULL OR prop.uuid IS NULL OR p.uuid <> prop.uuid
513
+ OR type(mid_diff_rel) <> "IS_RELATED" OR type(r_prop) <> "IS_RELATED"
514
+ )
486
515
  WITH path, prop, r_prop, mid_r_root
487
516
  ORDER BY
488
517
  type(r_prop),
489
518
  mid_r_root.branch = mid_diff_rel.branch DESC,
519
+ (mid_diff_rel.status = r_prop.status AND mid_diff_rel.branch = r_prop.branch) DESC,
490
520
  r_prop.from DESC,
491
521
  mid_r_root.from DESC
492
522
  WITH prop, type(r_prop) AS type_r_prop, head(collect(path)) AS latest_prop_path
493
523
  RETURN latest_prop_path
494
524
  }
495
525
  // -------------------------------------
496
- // Exclude properties within the timeframe
526
+ // Exclude properties added and deleted within the timeframe
497
527
  // -------------------------------------
498
528
  WITH q, nodes(latest_prop_path)[3] AS prop, type(relationships(latest_prop_path)[2]) AS rel_type, latest_prop_path, has_more_data, row_from_time
499
529
  CALL {
@@ -594,6 +624,14 @@ AND (
594
624
  )
595
625
  )
596
626
  )
627
+ // ------------------------
628
+ // special handling for nodes that had their kind updated,
629
+ // the migration leaves two nodes with the same UUID linked to the same Relationship
630
+ // ------------------------
631
+ AND (
632
+ n.uuid IS NULL OR q.uuid IS NULL OR n.uuid <> q.uuid
633
+ OR type(r_node) <> "IS_RELATED" OR type(diff_rel) <> "IS_RELATED"
634
+ )
597
635
  AND ALL(
598
636
  r_pair IN [[r_root, r_node], [r_node, diff_rel]]
599
637
  // filter out paths where a base branch edge follows a branch edge
@@ -203,15 +203,16 @@ class NodeCreateAllQuery(NodeQuery):
203
203
  }
204
204
  ipnetwork_prop_list = [f"{key}: {value}" for key, value in ipnetwork_prop.items()]
205
205
 
206
- query = """
207
- MATCH (root:Root)
208
- CREATE (n:Node:%(labels)s $node_prop )
209
- CREATE (n)-[r:IS_PART_OF $node_branch_prop ]->(root)
206
+ attrs_query = """
210
207
  WITH distinct n
211
- FOREACH ( attr IN $attrs |
208
+ UNWIND $attrs AS attr
209
+ CALL {
210
+ WITH n, attr
212
211
  CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
213
212
  CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
214
213
  MERGE (av:AttributeValue { value: attr.content.value, is_default: attr.content.is_default })
214
+ WITH n, attr, av, a
215
+ LIMIT 1
215
216
  CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
216
217
  MERGE (ip:Boolean { value: attr.is_protected })
217
218
  MERGE (iv:Boolean { value: attr.is_visible })
@@ -225,11 +226,19 @@ class NodeCreateAllQuery(NodeQuery):
225
226
  MERGE (peer:Node { uuid: prop.peer_id })
226
227
  CREATE (a)-[:HAS_OWNER { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(peer)
227
228
  )
228
- )
229
- FOREACH ( attr IN $attrs_iphost |
229
+ }"""
230
+
231
+ attrs_iphost_query = """
232
+ WITH distinct n
233
+ UNWIND $attrs_iphost AS attr_iphost
234
+ CALL {
235
+ WITH n, attr_iphost
236
+ WITH n, attr_iphost AS attr
230
237
  CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
231
238
  CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
232
239
  MERGE (av:AttributeValue:AttributeIPHost { %(iphost_prop)s })
240
+ WITH n, attr, av, a
241
+ LIMIT 1
233
242
  CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
234
243
  MERGE (ip:Boolean { value: attr.is_protected })
235
244
  MERGE (iv:Boolean { value: attr.is_visible })
@@ -243,11 +252,20 @@ class NodeCreateAllQuery(NodeQuery):
243
252
  MERGE (peer:Node { uuid: prop.peer_id })
244
253
  CREATE (a)-[:HAS_OWNER { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(peer)
245
254
  )
246
- )
247
- FOREACH ( attr IN $attrs_ipnetwork |
255
+ }
256
+ """ % {"iphost_prop": ", ".join(iphost_prop_list)}
257
+
258
+ attrs_ipnetwork_query = """
259
+ WITH distinct n
260
+ UNWIND $attrs_ipnetwork AS attr_ipnetwork
261
+ CALL {
262
+ WITH n, attr_ipnetwork
263
+ WITH n, attr_ipnetwork AS attr
248
264
  CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
249
265
  CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
250
266
  MERGE (av:AttributeValue:AttributeIPNetwork { %(ipnetwork_prop)s })
267
+ WITH n, attr, av, a
268
+ LIMIT 1
251
269
  CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
252
270
  MERGE (ip:Boolean { value: attr.is_protected })
253
271
  MERGE (iv:Boolean { value: attr.is_visible })
@@ -261,8 +279,14 @@ class NodeCreateAllQuery(NodeQuery):
261
279
  MERGE (peer:Node { uuid: prop.peer_id })
262
280
  CREATE (a)-[:HAS_OWNER { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(peer)
263
281
  )
264
- )
265
- FOREACH ( rel IN $rels_bidir |
282
+ }
283
+ """ % {"ipnetwork_prop": ", ".join(ipnetwork_prop_list)}
284
+
285
+ rels_bidir_query = """
286
+ WITH distinct n
287
+ UNWIND $rels_bidir AS rel
288
+ CALL {
289
+ WITH n, rel
266
290
  MERGE (d:Node { uuid: rel.destination_id })
267
291
  CREATE (rl:Relationship { uuid: rel.uuid, name: rel.name, branch_support: rel.branch_support })
268
292
  CREATE (n)-[:IS_RELATED %(rel_prop)s ]->(rl)
@@ -279,8 +303,15 @@ class NodeCreateAllQuery(NodeQuery):
279
303
  MERGE (peer:Node { uuid: prop.peer_id })
280
304
  CREATE (rl)-[:HAS_OWNER { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(peer)
281
305
  )
282
- )
283
- FOREACH ( rel IN $rels_out |
306
+ }
307
+ """ % {"rel_prop": rel_prop_str}
308
+
309
+ rels_out_query = """
310
+ WITH distinct n
311
+ UNWIND $rels_out AS rel_out
312
+ CALL {
313
+ WITH n, rel_out
314
+ WITH n, rel_out as rel
284
315
  MERGE (d:Node { uuid: rel.destination_id })
285
316
  CREATE (rl:Relationship { uuid: rel.uuid, name: rel.name, branch_support: rel.branch_support })
286
317
  CREATE (n)-[:IS_RELATED %(rel_prop)s ]->(rl)
@@ -297,8 +328,15 @@ class NodeCreateAllQuery(NodeQuery):
297
328
  MERGE (peer:Node { uuid: prop.peer_id })
298
329
  CREATE (rl)-[:HAS_OWNER { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(peer)
299
330
  )
300
- )
301
- FOREACH ( rel IN $rels_in |
331
+ }
332
+ """ % {"rel_prop": rel_prop_str}
333
+
334
+ rels_in_query = """
335
+ WITH distinct n
336
+ UNWIND $rels_in AS rel_in
337
+ CALL {
338
+ WITH n, rel_in
339
+ WITH n, rel_in AS rel
302
340
  MERGE (d:Node { uuid: rel.destination_id })
303
341
  CREATE (rl:Relationship { uuid: rel.uuid, name: rel.name, branch_support: rel.branch_support })
304
342
  CREATE (n)<-[:IS_RELATED %(rel_prop)s ]-(rl)
@@ -315,14 +353,23 @@ class NodeCreateAllQuery(NodeQuery):
315
353
  MERGE (peer:Node { uuid: prop.peer_id })
316
354
  CREATE (rl)-[:HAS_OWNER { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(peer)
317
355
  )
318
- )
356
+ }
357
+ """ % {"rel_prop": rel_prop_str}
358
+
359
+ query = f"""
360
+ MATCH (root:Root)
361
+ CREATE (n:Node:%(labels)s $node_prop )
362
+ CREATE (n)-[r:IS_PART_OF $node_branch_prop ]->(root)
363
+ {attrs_query if self.params["attrs"] else ""}
364
+ {attrs_iphost_query if self.params["attrs_iphost"] else ""}
365
+ {attrs_ipnetwork_query if self.params["attrs_ipnetwork"] else ""}
366
+ {rels_bidir_query if self.params["rels_bidir"] else ""}
367
+ {rels_out_query if self.params["rels_out"] else ""}
368
+ {rels_in_query if self.params["rels_in"] else ""}
319
369
  WITH distinct n
320
370
  MATCH (n)-[:HAS_ATTRIBUTE|IS_RELATED]-(rn)-[:HAS_VALUE|IS_RELATED]-(rv)
321
371
  """ % {
322
372
  "labels": ":".join(self.node.get_labels()),
323
- "rel_prop": rel_prop_str,
324
- "iphost_prop": ", ".join(iphost_prop_list),
325
- "ipnetwork_prop": ", ".join(ipnetwork_prop_list),
326
373
  }
327
374
 
328
375
  self.params["at"] = at.to_string()
@@ -479,11 +526,17 @@ class NodeListGetAttributeQuery(Query):
479
526
  self.return_labels = ["n", "a", "av", "r1", "r2"]
480
527
 
481
528
  # Add Is_Protected and Is_visible
529
+ rel_isv_branch_filter, _ = self.branch.get_query_filter_path(
530
+ at=self.at, branch_agnostic=self.branch_agnostic, variable_name="rel_isv"
531
+ )
532
+ rel_isp_branch_filter, _ = self.branch.get_query_filter_path(
533
+ at=self.at, branch_agnostic=self.branch_agnostic, variable_name="rel_isp"
534
+ )
482
535
  query = """
483
536
  MATCH (a)-[rel_isv:IS_VISIBLE]-(isv:Boolean)
484
537
  MATCH (a)-[rel_isp:IS_PROTECTED]-(isp:Boolean)
485
- WHERE all(r IN [rel_isv, rel_isp] WHERE ( %(branch_filter)s ))
486
- """ % {"branch_filter": branch_filter}
538
+ WHERE (%(rel_isv_branch_filter)s) AND (%(rel_isp_branch_filter)s)
539
+ """ % {"rel_isv_branch_filter": rel_isv_branch_filter, "rel_isp_branch_filter": rel_isp_branch_filter}
487
540
  self.add_to_query(query)
488
541
 
489
542
  self.return_labels.extend(["isv", "isp", "rel_isv", "rel_isp"])
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Generator, Optional, Union
8
8
  from infrahub_sdk.uuidt import UUIDT
9
9
 
10
10
  from infrahub.core.constants import RelationshipDirection, RelationshipStatus
11
+ from infrahub.core.constants.database import DatabaseEdgeType
11
12
  from infrahub.core.query import Query, QueryType
12
13
  from infrahub.core.query.subquery import build_subquery_filter, build_subquery_order
13
14
  from infrahub.core.timestamp import Timestamp
@@ -24,7 +25,7 @@ if TYPE_CHECKING:
24
25
  from infrahub.core.schema import RelationshipSchema
25
26
  from infrahub.database import InfrahubDatabase
26
27
 
27
- # pylint: disable=redefined-builtin
28
+ # pylint: disable=redefined-builtin,too-many-lines
28
29
 
29
30
 
30
31
  @dataclass
@@ -583,6 +584,7 @@ class RelationshipGetPeerQuery(Query):
583
584
  query = """
584
585
  MATCH (source_node:Node)%(arrow_left_start)s[:IS_RELATED]%(arrow_left_end)s(rl:Relationship { name: $rel_identifier })
585
586
  WHERE source_node.uuid IN $source_ids
587
+ WITH DISTINCT source_node, rl
586
588
  CALL {
587
589
  WITH rl, source_node
588
590
  MATCH path = (source_node)%(path)s(peer:Node)
@@ -659,10 +661,23 @@ class RelationshipGetPeerQuery(Query):
659
661
  # QUERY Properties
660
662
  # ----------------------------------------------------------------------------
661
663
  query = """
662
- MATCH (rl)-[rel_is_visible:IS_VISIBLE]-(is_visible)
663
- MATCH (rl)-[rel_is_protected:IS_PROTECTED]-(is_protected)
664
- WHERE all(r IN [ rel_is_visible, rel_is_protected] WHERE (%s))
665
- """ % (branch_filter,)
664
+ CALL {
665
+ WITH rl
666
+ MATCH (rl)-[r:IS_VISIBLE]-(is_visible)
667
+ WHERE %(branch_filter)s
668
+ RETURN r AS rel_is_visible, is_visible
669
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
670
+ LIMIT 1
671
+ }
672
+ CALL {
673
+ WITH rl
674
+ MATCH (rl)-[r:IS_PROTECTED]-(is_protected)
675
+ WHERE %(branch_filter)s
676
+ RETURN r AS rel_is_protected, is_protected
677
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
678
+ LIMIT 1
679
+ }
680
+ """ % {"branch_filter": branch_filter}
666
681
 
667
682
  self.add_to_query(query)
668
683
 
@@ -672,20 +687,24 @@ class RelationshipGetPeerQuery(Query):
672
687
  # We must query them one by one otherwise the second one won't return
673
688
  for node_prop in ["source", "owner"]:
674
689
  query = """
675
- WITH %s
676
- OPTIONAL MATCH (rl)-[rel_%s:HAS_%s]-(%s)
677
- WHERE all(r IN [ rel_%s ] WHERE (%s))
678
- """ % (
679
- ",".join(self.return_labels),
680
- node_prop,
681
- node_prop.upper(),
682
- node_prop,
683
- node_prop,
684
- branch_filter,
685
- )
690
+ CALL {
691
+ WITH rl
692
+ OPTIONAL MATCH (rl)-[r:HAS_%(node_prop_type)s]-(%(node_prop)s)
693
+ WHERE %(branch_filter)s
694
+ RETURN r AS rel_%(node_prop)s, %(node_prop)s
695
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
696
+ LIMIT 1
697
+ }
698
+ """ % {
699
+ "node_prop": node_prop,
700
+ "node_prop_type": node_prop.upper(),
701
+ "branch_filter": branch_filter,
702
+ }
686
703
  self.add_to_query(query)
687
704
  self.update_return_labels([f"rel_{node_prop}", node_prop])
688
705
 
706
+ self.add_to_query("WITH " + ",".join(self.return_labels))
707
+
689
708
  # ----------------------------------------------------------------------------
690
709
  # ORDER Results
691
710
  # ----------------------------------------------------------------------------
@@ -905,7 +924,8 @@ class RelationshipCountPerNodeQuery(Query):
905
924
  path = "<-[r:IS_RELATED]-"
906
925
 
907
926
  query = """
908
- MATCH (rl:Relationship { name: $rel_identifier })
927
+ MATCH (peer_node:Node)%(path)s(rl:Relationship { name: $rel_identifier })
928
+ WHERE peer_node.uuid IN $peer_ids AND %(branch_filter)s
909
929
  CALL {
910
930
  WITH rl
911
931
  MATCH path = (peer_node:Node)%(path)s(rl)
@@ -931,3 +951,73 @@ class RelationshipCountPerNodeQuery(Query):
931
951
  data[node_id] = 0
932
952
 
933
953
  return data
954
+
955
+
956
+ class RelationshipDeleteAllQuery(Query):
957
+ """
958
+ Delete all relationships linked to a given node on a given branch at a given time. For every IS_RELATED edge:
959
+ - Set `to` time if an active edge exist on the same branch.
960
+ - Create `deleted` edge.
961
+ - Apply above to every edges linked to any connected Relationship node.
962
+ """
963
+
964
+ name = "node_delete_all_relationships"
965
+ type = QueryType.WRITE
966
+ insert_return = False
967
+
968
+ def __init__(self, node_id: str, **kwargs):
969
+ self.node_id = node_id
970
+ super().__init__(**kwargs)
971
+
972
+ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None:
973
+ self.params["source_id"] = kwargs["node_id"]
974
+ self.params["branch"] = self.branch.name
975
+
976
+ self.params["rel_prop"] = {
977
+ "branch": self.branch.name,
978
+ "branch_level": self.branch.hierarchy_level,
979
+ "status": RelationshipStatus.DELETED.value,
980
+ "from": self.at.to_string(),
981
+ }
982
+
983
+ self.params["at"] = self.at.to_string()
984
+
985
+ active_rel_filter, rel_params = self.branch.get_query_filter_path(
986
+ at=self.at, variable_name="active_edge", branch_agnostic=self.branch_agnostic
987
+ )
988
+ self.params.update(rel_params)
989
+
990
+ query_match_relationships = """
991
+ MATCH (s:Node { uuid: $source_id })-[active_edge:IS_RELATED]-(rl:Relationship)
992
+ WHERE %(active_rel_filter)s AND active_edge.status = "active"
993
+ WITH DISTINCT rl
994
+ """ % {"active_rel_filter": active_rel_filter}
995
+
996
+ self.add_to_query(query_match_relationships)
997
+
998
+ for arrow_left, arrow_right in (("<-", "-"), ("-", "->")):
999
+ for edge_type in [
1000
+ DatabaseEdgeType.IS_RELATED.value,
1001
+ DatabaseEdgeType.IS_VISIBLE.value,
1002
+ DatabaseEdgeType.IS_PROTECTED.value,
1003
+ DatabaseEdgeType.HAS_OWNER.value,
1004
+ DatabaseEdgeType.HAS_SOURCE.value,
1005
+ ]:
1006
+ query = """
1007
+ CALL {
1008
+ WITH rl
1009
+ MATCH (rl)%(arrow_left)s[active_edge:%(edge_type)s]%(arrow_right)s(n)
1010
+ WHERE %(active_rel_filter)s AND active_edge.status ="active"
1011
+ CREATE (rl)%(arrow_left)s[deleted_edge:%(edge_type)s $rel_prop]%(arrow_right)s(n)
1012
+ SET deleted_edge.hierarchy = active_edge.hierarchy
1013
+ WITH active_edge
1014
+ WHERE active_edge.branch = $branch AND active_edge.to IS NULL
1015
+ SET active_edge.to = $at
1016
+ }
1017
+ """ % {
1018
+ "arrow_left": arrow_left,
1019
+ "arrow_right": arrow_right,
1020
+ "active_rel_filter": active_rel_filter,
1021
+ "edge_type": edge_type,
1022
+ }
1023
+ self.add_to_query(query)
@@ -123,6 +123,7 @@ class NumberPoolGetAllocated(Query):
123
123
  self.params["node_attribute"] = self.pool.node_attribute.value
124
124
  self.params["start_range"] = self.pool.start_range.value
125
125
  self.params["end_range"] = self.pool.end_range.value
126
+ self.params["pool_id"] = self.pool.get_id()
126
127
 
127
128
  branch_filter, branch_params = self.branch.get_query_filter_path(
128
129
  at=self.at.to_string(), branch_agnostic=self.branch_agnostic
@@ -133,7 +134,8 @@ class NumberPoolGetAllocated(Query):
133
134
  MATCH (n:%(node)s)-[ha:HAS_ATTRIBUTE]-(a:Attribute {name: $node_attribute})-[hv:HAS_VALUE]-(av:AttributeValue)
134
135
  MATCH (a)-[hs:HAS_SOURCE]-(pool:%(number_pool_kind)s)
135
136
  WHERE
136
- av.value >= $start_range and av.value <= $end_range
137
+ pool.uuid = $pool_id
138
+ AND av.value >= $start_range and av.value <= $end_range
137
139
  AND all(r in [ha, hv, hs] WHERE (%(branch_filter)s))
138
140
  AND ha.status = "active"
139
141
  AND hv.status = "active"
@@ -274,6 +276,8 @@ class NumberPoolSetReserved(Query):
274
276
  query = """
275
277
  MATCH (pool:%(number_pool)s { uuid: $pool_id })
276
278
  MERGE (value:AttributeValue { value: $reserved, is_default: false })
279
+ WITH value, pool
280
+ LIMIT 1
277
281
  CREATE (pool)-[rel:IS_RESERVED $rel_prop]->(value)
278
282
  """ % {"number_pool": InfrahubKind.NUMBERPOOL}
279
283