infrahub-server 1.3.1__py3-none-any.whl → 1.3.2__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.
@@ -181,8 +181,11 @@ class DiffCalculator:
181
181
  log.info("Diff property-level calculation queries for branch complete")
182
182
 
183
183
  if base_branch.name != diff_branch.name:
184
- current_node_field_specifiers = diff_parser.get_current_node_field_specifiers()
185
184
  new_node_field_specifiers = diff_parser.get_new_node_field_specifiers()
185
+ current_node_field_specifiers = None
186
+ if previous_node_specifiers is not None:
187
+ current_node_field_specifiers = previous_node_specifiers - new_node_field_specifiers
188
+
186
189
  base_calculation_request = DiffCalculationRequest(
187
190
  base_branch=base_branch,
188
191
  diff_branch=base_branch,
@@ -486,6 +486,7 @@ class DiffQueryParser:
486
486
  self._previous_node_field_specifiers = previous_node_field_specifiers or NodeFieldSpecifierMap()
487
487
  self._new_node_field_specifiers: NodeFieldSpecifierMap | None = None
488
488
  self._current_node_field_specifiers: NodeFieldSpecifierMap | None = None
489
+ self._diff_node_field_specifiers: NodeFieldSpecifierMap = NodeFieldSpecifierMap()
489
490
 
490
491
  def get_branches(self) -> set[str]:
491
492
  return set(self._final_diff_root_by_branch.keys())
@@ -497,33 +498,17 @@ class DiffQueryParser:
497
498
  return self._final_diff_root_by_branch[branch]
498
499
  return DiffRoot(from_time=self.from_time, to_time=self.to_time, uuid=str(uuid4()), branch=branch, nodes=[])
499
500
 
500
- def get_diff_node_field_specifiers(self) -> NodeFieldSpecifierMap:
501
- node_field_specifiers_map = NodeFieldSpecifierMap()
502
- if self.diff_branch_name not in self._diff_root_by_branch:
503
- return node_field_specifiers_map
504
- diff_root = self._diff_root_by_branch[self.diff_branch_name]
505
- for node in diff_root.nodes_by_identifier.values():
506
- for attribute_name in node.attributes_by_name:
507
- node_field_specifiers_map.add_entry(node_uuid=node.uuid, kind=node.kind, field_name=attribute_name)
508
- for relationship_diff in node.relationships_by_identifier.values():
509
- node_field_specifiers_map.add_entry(
510
- node_uuid=node.uuid, kind=node.kind, field_name=relationship_diff.identifier
511
- )
512
- return node_field_specifiers_map
513
-
514
501
  def get_new_node_field_specifiers(self) -> NodeFieldSpecifierMap:
515
- if self._new_node_field_specifiers is not None:
516
- return self._new_node_field_specifiers
517
- branch_node_specifiers = self.get_diff_node_field_specifiers()
518
- self._new_node_field_specifiers = branch_node_specifiers - self._previous_node_field_specifiers
519
- return self._new_node_field_specifiers
520
-
521
- def get_current_node_field_specifiers(self) -> NodeFieldSpecifierMap:
522
- if self._current_node_field_specifiers is not None:
523
- return self._current_node_field_specifiers
524
- new_node_field_specifiers = self.get_new_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
502
+ return self._diff_node_field_specifiers - self._previous_node_field_specifiers
503
+
504
+ def is_new_node_field_specifier(self, node_uuid: str, kind: str, field_name: str) -> bool:
505
+ if not self._diff_node_field_specifiers.has_entry(node_uuid=node_uuid, kind=kind, field_name=field_name):
506
+ return False
507
+ if self._previous_node_field_specifiers and self._previous_node_field_specifiers.has_entry(
508
+ node_uuid=node_uuid, kind=kind, field_name=field_name
509
+ ):
510
+ return False
511
+ return True
527
512
 
528
513
  def read_result(self, query_result: QueryResult) -> None:
529
514
  try:
@@ -533,8 +518,6 @@ class DiffQueryParser:
533
518
  return
534
519
  database_path = DatabasePath.from_cypher_path(cypher_path=path)
535
520
  self._parse_path(database_path=database_path)
536
- self._current_node_field_specifiers = None
537
- self._new_node_field_specifiers = None
538
521
 
539
522
  def parse(self, include_unchanged: bool = False) -> None:
540
523
  self._new_node_field_specifiers = None
@@ -617,11 +600,15 @@ class DiffQueryParser:
617
600
  branch_name = database_path.deepest_branch
618
601
  from_time = self.from_time
619
602
  if branch_name == self.base_branch_name:
620
- new_node_field_specifiers = self.get_new_node_field_specifiers()
621
- if new_node_field_specifiers.has_entry(
603
+ if self.is_new_node_field_specifier(
622
604
  node_uuid=diff_node.uuid, kind=diff_node.kind, field_name=attribute_name
623
605
  ):
624
606
  from_time = self.diff_branched_from_time
607
+ else:
608
+ # Add to diff node field specifiers if this is the diff branch
609
+ self._diff_node_field_specifiers.add_entry(
610
+ node_uuid=diff_node.uuid, kind=diff_node.kind, field_name=attribute_name
611
+ )
625
612
  if attribute_name not in diff_node.attributes_by_name:
626
613
  diff_node.attributes_by_name[attribute_name] = DiffAttributeIntermediate(
627
614
  uuid=database_path.attribute_id,
@@ -663,11 +650,15 @@ class DiffQueryParser:
663
650
  branch_name = database_path.deepest_branch
664
651
  from_time = self.from_time
665
652
  if branch_name == self.base_branch_name:
666
- new_node_field_specifiers = self.get_new_node_field_specifiers()
667
- if new_node_field_specifiers.has_entry(
653
+ if self.is_new_node_field_specifier(
668
654
  node_uuid=diff_node.uuid, kind=diff_node.kind, field_name=relationship_schema.get_identifier()
669
655
  ):
670
656
  from_time = self.diff_branched_from_time
657
+ else:
658
+ # Add to diff node field specifiers if this is the diff branch
659
+ self._diff_node_field_specifiers.add_entry(
660
+ node_uuid=diff_node.uuid, kind=diff_node.kind, field_name=relationship_schema.get_identifier()
661
+ )
671
662
  diff_relationship = DiffRelationshipIntermediate(
672
663
  name=relationship_schema.name,
673
664
  cardinality=relationship_schema.cardinality,
@@ -1,26 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Iterable
3
+ from typing import TYPE_CHECKING
4
4
 
5
5
  from infrahub.core import registry
6
6
  from infrahub.core.constants import NULL_VALUE
7
- from infrahub.core.schema import (
8
- MainSchemaTypes,
9
- SchemaAttributePath,
10
- SchemaAttributePathValue,
11
- )
12
7
  from infrahub.core.schema.basenode_schema import (
13
- SchemaUniquenessConstraintPath,
14
8
  UniquenessConstraintType,
15
9
  UniquenessConstraintViolation,
16
10
  )
17
- from infrahub.core.validators.uniqueness.index import UniquenessQueryResultsIndex
18
11
  from infrahub.core.validators.uniqueness.model import (
19
- NodeUniquenessQueryRequest,
20
- QueryAttributePath,
21
- QueryRelationshipAttributePath,
12
+ NodeUniquenessQueryRequestValued,
13
+ QueryAttributePathValued,
14
+ QueryRelationshipPathValued,
22
15
  )
23
- from infrahub.core.validators.uniqueness.query import NodeUniqueAttributeConstraintQuery
16
+ from infrahub.core.validators.uniqueness.query import UniquenessValidationQuery
24
17
  from infrahub.exceptions import HFIDViolatedError, ValidationError
25
18
 
26
19
  from .interface import NodeConstraintInterface
@@ -28,8 +21,11 @@ from .interface import NodeConstraintInterface
28
21
  if TYPE_CHECKING:
29
22
  from infrahub.core.branch import Branch
30
23
  from infrahub.core.node import Node
31
- from infrahub.core.query import QueryResult
32
24
  from infrahub.core.relationship.model import RelationshipManager
25
+ from infrahub.core.schema import (
26
+ MainSchemaTypes,
27
+ SchemaAttributePath,
28
+ )
33
29
  from infrahub.core.timestamp import Timestamp
34
30
  from infrahub.database import InfrahubDatabase
35
31
 
@@ -40,72 +36,38 @@ class NodeGroupedUniquenessConstraint(NodeConstraintInterface):
40
36
  self.branch = branch
41
37
  self.schema_branch = registry.schema.get_schema_branch(branch.name)
42
38
 
43
- async def _build_query_request(
44
- self,
45
- updated_node: Node,
46
- node_schema: MainSchemaTypes,
47
- uniqueness_constraint_paths: list[SchemaUniquenessConstraintPath],
48
- filters: list[str] | None = None,
49
- ) -> NodeUniquenessQueryRequest:
50
- query_request = NodeUniquenessQueryRequest(kind=node_schema.kind)
51
- for uniqueness_constraint_path in uniqueness_constraint_paths:
52
- include_in_query = not filters
53
- query_relationship_paths: set[QueryRelationshipAttributePath] = set()
54
- query_attribute_paths: set[QueryAttributePath] = set()
55
- for attribute_path in uniqueness_constraint_path.attributes_paths:
56
- if attribute_path.related_schema and attribute_path.relationship_schema:
57
- if filters and attribute_path.relationship_schema.name in filters:
58
- include_in_query = True
59
-
60
- relationship_manager: RelationshipManager = getattr(
61
- updated_node, attribute_path.relationship_schema.name
62
- )
63
- related_node = await relationship_manager.get_peer(db=self.db)
64
- related_node_id = related_node.get_id() if related_node else None
65
- query_relationship_paths.add(
66
- QueryRelationshipAttributePath(
67
- identifier=attribute_path.relationship_schema.get_identifier(),
68
- value=related_node_id,
69
- )
70
- )
71
- continue
72
- if attribute_path.attribute_schema:
73
- if filters and attribute_path.attribute_schema.name in filters:
74
- include_in_query = True
75
- attribute_name = attribute_path.attribute_schema.name
76
- attribute = getattr(updated_node, attribute_name)
77
- if attribute.is_enum and attribute.value:
78
- attribute_value = attribute.value.value
79
- else:
80
- attribute_value = attribute.value
81
- if attribute_value is None:
82
- attribute_value = NULL_VALUE
83
- query_attribute_paths.add(
84
- QueryAttributePath(
85
- attribute_name=attribute_name,
86
- property_name=attribute_path.attribute_property_name or "value",
87
- value=attribute_value,
88
- )
89
- )
90
- if include_in_query:
91
- query_request.relationship_attribute_paths |= query_relationship_paths
92
- query_request.unique_attribute_paths |= query_attribute_paths
93
- return query_request
94
-
95
- async def _get_node_attribute_path_values(
39
+ async def _get_unique_valued_paths(
96
40
  self,
97
41
  updated_node: Node,
98
42
  path_group: list[SchemaAttributePath],
99
- ) -> list[SchemaAttributePathValue]:
100
- node_value_combination = []
43
+ filters: list[str],
44
+ ) -> list[QueryAttributePathValued | QueryRelationshipPathValued]:
45
+ # if filters are provided, we need to check if the path group is relevant to the filters
46
+ if filters:
47
+ field_names: list[str] = []
48
+ for schema_attribute_path in path_group:
49
+ if schema_attribute_path.relationship_schema:
50
+ field_names.append(schema_attribute_path.relationship_schema.name)
51
+ elif schema_attribute_path.attribute_schema:
52
+ field_names.append(schema_attribute_path.attribute_schema.name)
53
+
54
+ if not set(field_names) & set(filters):
55
+ return []
56
+
57
+ valued_paths: list[QueryAttributePathValued | QueryRelationshipPathValued] = []
101
58
  for schema_attribute_path in path_group:
102
59
  if schema_attribute_path.relationship_schema:
103
60
  relationship_name = schema_attribute_path.relationship_schema.name
104
61
  relationship_manager: RelationshipManager = getattr(updated_node, relationship_name)
105
62
  related_node = await relationship_manager.get_peer(db=self.db)
106
63
  related_node_id = related_node.get_id() if related_node else None
107
- node_value_combination.append(
108
- SchemaAttributePathValue.from_schema_attribute_path(schema_attribute_path, value=related_node_id)
64
+ valued_paths.append(
65
+ QueryRelationshipPathValued(
66
+ relationship_schema=schema_attribute_path.relationship_schema,
67
+ peer_id=related_node_id,
68
+ attribute_name=None,
69
+ attribute_value=None,
70
+ )
109
71
  )
110
72
  elif schema_attribute_path.attribute_schema:
111
73
  attribute_name = schema_attribute_path.attribute_schema.name
@@ -115,86 +77,79 @@ class NodeGroupedUniquenessConstraint(NodeConstraintInterface):
115
77
  attribute_value = attribute_value.value
116
78
  elif attribute_value is None:
117
79
  attribute_value = NULL_VALUE
118
- node_value_combination.append(
119
- SchemaAttributePathValue.from_schema_attribute_path(
120
- schema_attribute_path,
80
+ valued_paths.append(
81
+ QueryAttributePathValued(
82
+ attribute_name=attribute_name,
121
83
  value=attribute_value,
122
84
  )
123
85
  )
124
- return node_value_combination
86
+ return valued_paths
125
87
 
126
- async def _get_violations(
88
+ async def _get_single_schema_violations(
127
89
  self,
128
- updated_node: Node,
129
- uniqueness_constraint_paths: list[SchemaUniquenessConstraintPath],
130
- query_results: Iterable[QueryResult],
90
+ node: Node,
91
+ node_schema: MainSchemaTypes,
92
+ filters: list[str],
93
+ at: Timestamp | None = None,
131
94
  ) -> list[UniquenessConstraintViolation]:
132
- results_index = UniquenessQueryResultsIndex(
133
- query_results=query_results, exclude_node_ids={updated_node.get_id()}
95
+ schema_branch = self.db.schema.get_schema_branch(name=self.branch.name)
96
+
97
+ uniqueness_constraint_paths = node_schema.get_unique_constraint_schema_attribute_paths(
98
+ schema_branch=schema_branch
134
99
  )
135
- violations = []
100
+
101
+ violations: list[UniquenessConstraintViolation] = []
136
102
  for uniqueness_constraint_path in uniqueness_constraint_paths:
137
- # path_group = one constraint (that can contain multiple items)
138
- schema_attribute_path_values = await self._get_node_attribute_path_values(
139
- updated_node=updated_node, path_group=uniqueness_constraint_path.attributes_paths
103
+ valued_paths = await self._get_unique_valued_paths(
104
+ updated_node=node,
105
+ path_group=uniqueness_constraint_path.attributes_paths,
106
+ filters=filters,
140
107
  )
141
108
 
142
- # constraint cannot be violated if this node is missing any values
143
- if any(sapv.value is None for sapv in schema_attribute_path_values):
109
+ if not valued_paths:
144
110
  continue
145
111
 
146
- matching_node_ids = results_index.get_node_ids_for_value_group(schema_attribute_path_values)
147
- if not matching_node_ids:
112
+ # Create the valued query request for this constraint
113
+ valued_query_request = NodeUniquenessQueryRequestValued(
114
+ kind=node_schema.kind,
115
+ unique_valued_paths=valued_paths,
116
+ )
117
+
118
+ # Execute the query
119
+ query = await UniquenessValidationQuery.init(
120
+ db=self.db,
121
+ branch=self.branch,
122
+ at=at,
123
+ query_request=valued_query_request,
124
+ node_ids_to_exclude=[node.get_id()],
125
+ )
126
+ await query.execute(db=self.db)
127
+
128
+ # Get violation nodes from the query results
129
+ violation_nodes = query.get_violation_nodes()
130
+ if not violation_nodes:
148
131
  continue
149
132
 
133
+ # Create violation object
150
134
  uniqueness_constraint_fields = []
151
- for sapv in schema_attribute_path_values:
152
- if sapv.relationship_schema:
153
- uniqueness_constraint_fields.append(sapv.relationship_schema.name)
154
- elif sapv.attribute_schema:
155
- uniqueness_constraint_fields.append(sapv.attribute_schema.name)
156
-
157
- violations.append(
158
- UniquenessConstraintViolation(
159
- nodes_ids=matching_node_ids,
160
- fields=uniqueness_constraint_fields,
161
- typ=uniqueness_constraint_path.typ,
135
+ for valued_path in valued_paths:
136
+ if isinstance(valued_path, QueryRelationshipPathValued):
137
+ uniqueness_constraint_fields.append(valued_path.relationship_schema.name)
138
+ elif isinstance(valued_path, QueryAttributePathValued):
139
+ uniqueness_constraint_fields.append(valued_path.attribute_name)
140
+
141
+ matching_node_ids = {node_id for node_id, _ in violation_nodes}
142
+ if matching_node_ids:
143
+ violations.append(
144
+ UniquenessConstraintViolation(
145
+ nodes_ids=matching_node_ids,
146
+ fields=uniqueness_constraint_fields,
147
+ typ=uniqueness_constraint_path.typ,
148
+ )
162
149
  )
163
- )
164
150
 
165
151
  return violations
166
152
 
167
- async def _get_single_schema_violations(
168
- self,
169
- node: Node,
170
- node_schema: MainSchemaTypes,
171
- at: Timestamp | None = None,
172
- filters: list[str] | None = None,
173
- ) -> list[UniquenessConstraintViolation]:
174
- schema_branch = self.db.schema.get_schema_branch(name=self.branch.name)
175
-
176
- uniqueness_constraint_paths = node_schema.get_unique_constraint_schema_attribute_paths(
177
- schema_branch=schema_branch
178
- )
179
- query_request = await self._build_query_request(
180
- updated_node=node,
181
- node_schema=node_schema,
182
- uniqueness_constraint_paths=uniqueness_constraint_paths,
183
- filters=filters,
184
- )
185
- if not query_request:
186
- return []
187
-
188
- query = await NodeUniqueAttributeConstraintQuery.init(
189
- db=self.db, branch=self.branch, at=at, query_request=query_request, min_count_required=0
190
- )
191
- await query.execute(db=self.db)
192
- return await self._get_violations(
193
- updated_node=node,
194
- uniqueness_constraint_paths=uniqueness_constraint_paths,
195
- query_results=query.get_results(),
196
- )
197
-
198
153
  async def check(self, node: Node, at: Timestamp | None = None, filters: list[str] | None = None) -> None:
199
154
  def _frozen_constraints(schema: MainSchemaTypes) -> frozenset[frozenset[str]]:
200
155
  if not schema.uniqueness_constraints:
@@ -218,7 +173,8 @@ class NodeGroupedUniquenessConstraint(NodeConstraintInterface):
218
173
  if include_node_schema:
219
174
  schemas_to_check.append(node_schema)
220
175
 
221
- violations = []
176
+ violations: list[UniquenessConstraintViolation] = []
177
+
222
178
  for schema in schemas_to_check:
223
179
  schema_filters = list(filters) if filters is not None else []
224
180
  for attr_schema in schema.attributes:
@@ -59,6 +59,23 @@ class NodeUniquenessQueryRequest(BaseModel):
59
59
  )
60
60
 
61
61
 
62
+ class QueryRelationshipPathValued(BaseModel):
63
+ relationship_schema: RelationshipSchema
64
+ peer_id: str | None
65
+ attribute_name: str | None
66
+ attribute_value: str | bool | int | float | None
67
+
68
+
69
+ class QueryAttributePathValued(BaseModel):
70
+ attribute_name: str
71
+ value: str | bool | int | float
72
+
73
+
74
+ class NodeUniquenessQueryRequestValued(BaseModel):
75
+ kind: str
76
+ unique_valued_paths: list[QueryAttributePathValued | QueryRelationshipPathValued]
77
+
78
+
62
79
  class NonUniqueRelatedAttribute(BaseModel):
63
80
  relationship: RelationshipSchema
64
81
  attribute_name: str
@@ -5,10 +5,12 @@ from typing import TYPE_CHECKING, Any
5
5
  from infrahub.core.constants.relationship_label import RELATIONSHIP_TO_VALUE_LABEL
6
6
  from infrahub.core.query import Query, QueryType
7
7
 
8
+ from .model import QueryAttributePathValued, QueryRelationshipPathValued
9
+
8
10
  if TYPE_CHECKING:
9
11
  from infrahub.database import InfrahubDatabase
10
12
 
11
- from .model import NodeUniquenessQueryRequest
13
+ from .model import NodeUniquenessQueryRequest, NodeUniquenessQueryRequestValued
12
14
 
13
15
 
14
16
  class NodeUniqueAttributeConstraintQuery(Query):
@@ -244,3 +246,212 @@ class NodeUniqueAttributeConstraintQuery(Query):
244
246
  "attr_value",
245
247
  "relationship_identifier",
246
248
  ]
249
+
250
+
251
+ class UniquenessValidationQuery(Query):
252
+ name = "uniqueness_constraint_validation"
253
+ type = QueryType.READ
254
+
255
+ def __init__(
256
+ self,
257
+ query_request: NodeUniquenessQueryRequestValued,
258
+ node_ids_to_exclude: list[str] | None = None,
259
+ **kwargs: Any,
260
+ ) -> None:
261
+ self.query_request = query_request
262
+ self.node_ids_to_exclude = node_ids_to_exclude
263
+ super().__init__(**kwargs)
264
+
265
+ def _build_attr_subquery(
266
+ self, node_kind: str, attr_path: QueryAttributePathValued, index: int, branch_filter: str, is_first_query: bool
267
+ ) -> tuple[str, dict[str, str | int | float | bool]]:
268
+ attr_name_var = f"attr_name_{index}"
269
+ attr_value_var = f"attr_value_{index}"
270
+ if is_first_query:
271
+ first_query_filter = "WHERE $node_ids_to_exclude IS NULL OR NOT node.uuid IN $node_ids_to_exclude"
272
+ else:
273
+ first_query_filter = ""
274
+ attribute_query = """
275
+ MATCH (node:%(node_kind)s)-[:HAS_ATTRIBUTE]->(attr:Attribute {name: $%(attr_name_var)s})-[:HAS_VALUE]->(:AttributeValue {value: $%(attr_value_var)s})
276
+ %(first_query_filter)s
277
+ WITH DISTINCT node
278
+ CALL (node) {
279
+ MATCH (node)-[r:HAS_ATTRIBUTE]->(attr:Attribute {name: $%(attr_name_var)s})
280
+ WHERE %(branch_filter)s
281
+ WITH attr, r.status = "active" AS is_active
282
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
283
+ WITH attr, is_active
284
+ LIMIT 1
285
+ WITH attr, is_active
286
+ WHERE is_active = TRUE
287
+ MATCH (attr)-[r:HAS_VALUE]->(:AttributeValue {value: $%(attr_value_var)s})
288
+ WHERE %(branch_filter)s
289
+ WITH r
290
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
291
+ LIMIT 1
292
+ WITH r
293
+ WHERE r.status = "active"
294
+ RETURN 1 AS is_match_%(index)s
295
+ }
296
+ """ % {
297
+ "first_query_filter": first_query_filter,
298
+ "node_kind": node_kind,
299
+ "attr_name_var": attr_name_var,
300
+ "attr_value_var": attr_value_var,
301
+ "branch_filter": branch_filter,
302
+ "index": index,
303
+ }
304
+ params: dict[str, str | int | float | bool] = {
305
+ attr_name_var: attr_path.attribute_name,
306
+ attr_value_var: attr_path.value,
307
+ }
308
+ return attribute_query, params
309
+
310
+ def _build_rel_subquery(
311
+ self,
312
+ node_kind: str,
313
+ rel_path: QueryRelationshipPathValued,
314
+ index: int,
315
+ branch_filter: str,
316
+ is_first_query: bool,
317
+ ) -> tuple[str, dict[str, str | int | float | bool]]:
318
+ params: dict[str, str | int | float | bool] = {}
319
+ rel_attr_query = ""
320
+ rel_attr_match = ""
321
+ if rel_path.attribute_name and rel_path.attribute_value:
322
+ attr_name_var = f"attr_name_{index}"
323
+ attr_value_var = f"attr_value_{index}"
324
+ rel_attr_query = """
325
+ MATCH (peer)-[r:HAS_ATTRIBUTE]->(attr:Attribute {name: $%(attr_name_var)s})
326
+ WHERE %(branch_filter)s
327
+ WITH attr, r.status = "active" AS is_active
328
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
329
+ WITH attr, is_active
330
+ LIMIT 1
331
+ WITH attr, is_active
332
+ WHERE is_active = TRUE
333
+ MATCH (attr)-[r:HAS_VALUE]->(:AttributeValue {value: $%(attr_value_var)s})
334
+ WHERE %(branch_filter)s
335
+ WITH r
336
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
337
+ LIMIT 1
338
+ WITH r
339
+ WHERE r.status = "active"
340
+ """ % {"attr_name_var": attr_name_var, "attr_value_var": attr_value_var, "branch_filter": branch_filter}
341
+ rel_attr_match = (
342
+ "-[r:HAS_ATTRIBUTE]->(attr:Attribute {name: $%(attr_name_var)s})-[:HAS_VALUE]->(:AttributeValue {value: $%(attr_value_var)s})"
343
+ % {
344
+ "attr_name_var": attr_name_var,
345
+ "attr_value_var": attr_value_var,
346
+ }
347
+ )
348
+ params[attr_name_var] = rel_path.attribute_name
349
+ params[attr_value_var] = rel_path.attribute_value
350
+ query_arrows = rel_path.relationship_schema.get_query_arrows()
351
+ rel_name_var = f"rel_name_{index}"
352
+ # long path MATCH is required to hit an index on the peer or AttributeValue of the peer
353
+ first_match = (
354
+ "MATCH (node:%(node_kind)s)%(lstart)s[:IS_RELATED]%(lend)s(:Relationship {name: $%(rel_name_var)s})%(rstart)s[:IS_RELATED]%(rend)s"
355
+ % {
356
+ "node_kind": node_kind,
357
+ "lstart": query_arrows.left.start,
358
+ "lend": query_arrows.left.end,
359
+ "rstart": query_arrows.right.start,
360
+ "rend": query_arrows.right.end,
361
+ "rel_name_var": rel_name_var,
362
+ }
363
+ )
364
+ peer_where = f"WHERE {branch_filter}"
365
+ if rel_path.peer_id:
366
+ peer_id_var = f"peer_id_{index}"
367
+ peer_where += f" AND peer.uuid = ${peer_id_var}"
368
+ params[peer_id_var] = rel_path.peer_id
369
+ first_match += "(:Node {uuid: $%(peer_id_var)s})" % {"peer_id_var": peer_id_var}
370
+ else:
371
+ peer_where += " AND peer.uuid <> node.uuid"
372
+ first_match += "(:Node)"
373
+ if rel_attr_match:
374
+ first_match += rel_attr_match
375
+ if is_first_query:
376
+ first_query_filter = "WHERE $node_ids_to_exclude IS NULL OR NOT node.uuid IN $node_ids_to_exclude"
377
+ else:
378
+ first_query_filter = ""
379
+ relationship_query = f"""
380
+ {first_match}
381
+ {first_query_filter}
382
+ WITH DISTINCT node
383
+ """
384
+ relationship_query += """
385
+ CALL (node) {
386
+ MATCH (node)%(lstart)s[r:IS_RELATED]%(lend)s(rel:Relationship {name: $%(rel_name_var)s})
387
+ WHERE %(branch_filter)s
388
+ WITH rel, r.status = "active" AS is_active
389
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
390
+ WITH rel, is_active
391
+ LIMIT 1
392
+ WITH rel, is_active
393
+ WHERE is_active = TRUE
394
+ MATCH (rel)%(rstart)s[r:IS_RELATED]%(rend)s(peer:Node)
395
+ %(peer_where)s
396
+ WITH peer, r.status = "active" AS is_active
397
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
398
+ WITH peer, is_active
399
+ LIMIT 1
400
+ WITH peer, is_active
401
+ WHERE is_active = TRUE
402
+ %(rel_attr_query)s
403
+ RETURN 1 AS is_match_%(index)s
404
+ LIMIT 1
405
+ }
406
+ """ % {
407
+ "rel_name_var": rel_name_var,
408
+ "lstart": query_arrows.left.start,
409
+ "lend": query_arrows.left.end,
410
+ "rstart": query_arrows.right.start,
411
+ "rend": query_arrows.right.end,
412
+ "peer_where": peer_where,
413
+ "rel_attr_query": rel_attr_query,
414
+ "branch_filter": branch_filter,
415
+ "index": index,
416
+ }
417
+ params[rel_name_var] = rel_path.relationship_schema.get_identifier()
418
+ return relationship_query, params
419
+
420
+ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
421
+ self.params["node_ids_to_exclude"] = self.node_ids_to_exclude
422
+ branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at.to_string(), is_isolated=False)
423
+ self.params.update(branch_params)
424
+
425
+ subqueries = []
426
+ for index, schema_path in enumerate(self.query_request.unique_valued_paths):
427
+ is_first_query = index == 0
428
+ if isinstance(schema_path, QueryAttributePathValued):
429
+ subquery, params = self._build_attr_subquery(
430
+ node_kind=self.query_request.kind,
431
+ attr_path=schema_path,
432
+ index=index,
433
+ branch_filter=branch_filter,
434
+ is_first_query=is_first_query,
435
+ )
436
+ else:
437
+ subquery, params = self._build_rel_subquery(
438
+ node_kind=self.query_request.kind,
439
+ rel_path=schema_path,
440
+ index=index,
441
+ branch_filter=branch_filter,
442
+ is_first_query=is_first_query,
443
+ )
444
+ subqueries.append(subquery)
445
+ self.params.update(params)
446
+
447
+ full_query = "\n".join(subqueries)
448
+ self.add_to_query(full_query)
449
+ self.return_labels = ["node.uuid AS node_uuid", "node.kind AS node_kind"]
450
+
451
+ def get_violation_nodes(self) -> list[tuple[str, str]]:
452
+ violation_tuples = []
453
+ for result in self.results:
454
+ violation_tuples.append(
455
+ (result.get_as_type("node_uuid", return_type=str), result.get_as_type("node_kind", return_type=str))
456
+ )
457
+ return violation_tuples
@@ -25,7 +25,7 @@ from infrahub.core.timestamp import Timestamp
25
25
  from infrahub.database import retry_db_transaction
26
26
  from infrahub.dependencies.registry import get_component_registry
27
27
  from infrahub.events.generator import generate_node_mutation_events
28
- from infrahub.exceptions import HFIDViolatedError, InitializationError
28
+ from infrahub.exceptions import HFIDViolatedError, InitializationError, NodeNotFoundError
29
29
  from infrahub.graphql.context import apply_external_context
30
30
  from infrahub.lock import InfrahubMultiLock, build_object_lock_name
31
31
  from infrahub.log import get_log_data, get_logger
@@ -384,7 +384,23 @@ class InfrahubMutationMixin:
384
384
  if len(exc.matching_nodes_ids) > 1:
385
385
  raise RuntimeError(f"Multiple {schema_name} nodes have the same hfid") from exc
386
386
  node_id = list(exc.matching_nodes_ids)[0]
387
- node = await NodeManager.get_one(db=db, id=node_id, kind=schema_name, branch=branch, raise_on_error=True)
387
+
388
+ try:
389
+ node = await NodeManager.get_one(
390
+ db=db, id=node_id, kind=schema_name, branch=branch, raise_on_error=True
391
+ )
392
+ except NodeNotFoundError as exc:
393
+ if branch.is_default:
394
+ raise
395
+ raise NodeNotFoundError(
396
+ node_type=exc.node_type,
397
+ identifier=exc.identifier,
398
+ branch_name=branch.name,
399
+ message=(
400
+ f"Node {exc.identifier} / {exc.node_type} uses this human-friendly ID, but does not exist on"
401
+ f" this branch. Please rebase this branch to access {exc.identifier} / {exc.node_type}"
402
+ ),
403
+ ) from exc
388
404
  updated_obj, mutation = await cls._call_mutate_update(
389
405
  info=info,
390
406
  data=data,
infrahub_sdk/node/node.py CHANGED
@@ -507,11 +507,17 @@ class InfrahubNode(InfrahubNodeBase):
507
507
 
508
508
  if rel_schema.cardinality == "one":
509
509
  if isinstance(rel_data, RelatedNode):
510
- peer_id_data: dict[str, Any] = {}
511
- if rel_data.id:
512
- peer_id_data["id"] = rel_data.id
513
- if rel_data.hfid:
514
- peer_id_data["hfid"] = rel_data.hfid
510
+ peer_id_data: dict[str, Any] = {
511
+ key: value
512
+ for key, value in (
513
+ ("id", rel_data.id),
514
+ ("hfid", rel_data.hfid),
515
+ ("__typename", rel_data.typename),
516
+ ("kind", rel_data.kind),
517
+ ("display_label", rel_data.display_label),
518
+ )
519
+ if value is not None
520
+ }
515
521
  if peer_id_data:
516
522
  rel_data = peer_id_data
517
523
  else:
@@ -1090,11 +1096,17 @@ class InfrahubNodeSync(InfrahubNodeBase):
1090
1096
 
1091
1097
  if rel_schema.cardinality == "one":
1092
1098
  if isinstance(rel_data, RelatedNodeSync):
1093
- peer_id_data: dict[str, Any] = {}
1094
- if rel_data.id:
1095
- peer_id_data["id"] = rel_data.id
1096
- if rel_data.hfid:
1097
- peer_id_data["hfid"] = rel_data.hfid
1099
+ peer_id_data: dict[str, Any] = {
1100
+ key: value
1101
+ for key, value in (
1102
+ ("id", rel_data.id),
1103
+ ("hfid", rel_data.hfid),
1104
+ ("__typename", rel_data.typename),
1105
+ ("kind", rel_data.kind),
1106
+ ("display_label", rel_data.display_label),
1107
+ )
1108
+ if value is not None
1109
+ }
1098
1110
  if peer_id_data:
1099
1111
  rel_data = peer_id_data
1100
1112
  else:
@@ -39,6 +39,7 @@ class RelatedNodeBase:
39
39
  self._hfid: list[str] | None = None
40
40
  self._display_label: str | None = None
41
41
  self._typename: str | None = None
42
+ self._kind: str | None = None
42
43
 
43
44
  if isinstance(data, (CoreNodeBase)):
44
45
  self._peer = data
@@ -118,6 +119,12 @@ class RelatedNodeBase:
118
119
  return self._peer.typename
119
120
  return self._typename
120
121
 
122
+ @property
123
+ def kind(self) -> str | None:
124
+ if self._peer:
125
+ return self._peer.get_kind()
126
+ return self._kind
127
+
121
128
  def _generate_input_data(self, allocate_from_pool: bool = False) -> dict[str, Any]:
122
129
  data: dict[str, Any] = {}
123
130
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: infrahub-server
3
- Version: 1.3.1
3
+ Version: 1.3.2
4
4
  Summary: Infrahub is taking a new approach to Infrastructure Management by providing a new generation of datastore to organize and control all the data that defines how an infrastructure should run.
5
5
  License: AGPL-3.0-only
6
6
  Author: OpsMill
@@ -80,7 +80,7 @@ infrahub/core/diff/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuF
80
80
  infrahub/core/diff/artifacts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
81
81
  infrahub/core/diff/artifacts/calculator.py,sha256=qk1DspB3bkKeWJFesLbmziCALVnbRadjrez1kn_IZWU,4435
82
82
  infrahub/core/diff/branch_differ.py,sha256=62TRs3tGb4brQqCaVoI2iMIiPnny3_0_e9j-Mq-AXx4,7752
83
- infrahub/core/diff/calculator.py,sha256=Wn0IRqCujwfXXNQbi6n-npEE0UrcWi5qq58wAIBMZsM,9976
83
+ infrahub/core/diff/calculator.py,sha256=1ure9OGmoopCLMyjIK4r6zmLJ4eAswEI7yB0WgOkgx0,10088
84
84
  infrahub/core/diff/combiner.py,sha256=qL4WQsphB2sVnncgskSG_QcJBqBHjaK0vWU_apeTn-E,23508
85
85
  infrahub/core/diff/conflict_transferer.py,sha256=LZCuS9Dbr4yBf-bd3RF-9cPnaOvVWiU3KBmmwxbRZl0,3968
86
86
  infrahub/core/diff/conflicts_enricher.py,sha256=x6qiZOXO2A3BQ2Fm78apJ4WA7HLzPO84JomJfcyuyDg,12552
@@ -127,7 +127,7 @@ infrahub/core/diff/query/save.py,sha256=xBKWpWfRWfaP7g523xKMK82ogg0AfVQTTMeyz8oe
127
127
  infrahub/core/diff/query/summary_counts_enricher.py,sha256=HuMeQfa2Ce0qFmGTSfUV-LncauEsBDhdDcs1QpZOETA,9957
128
128
  infrahub/core/diff/query/time_range_query.py,sha256=Xv9_Y_UJ45UsqfxosoAxXMY47-EpO6fHNIqdwFpysBQ,2976
129
129
  infrahub/core/diff/query/update_conflict_query.py,sha256=kQkFazz88wnApr8UU_qb0ruzhmrhWiqhbErukSAMhLA,1212
130
- infrahub/core/diff/query_parser.py,sha256=6OWI_ynplysO6VH1vCfqiV_VmXvAfoa-bIRJ7heVTjY,37217
130
+ infrahub/core/diff/query_parser.py,sha256=-uz4Fd2briFx7447UGii4RBSJjP_LnV0WiEBOjh_chc,36536
131
131
  infrahub/core/diff/repository/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
132
132
  infrahub/core/diff/repository/deserializer.py,sha256=bhN9ao8HxqKyRz273QGLNV9z9_SS4EQnM9JoY5ptx78,21337
133
133
  infrahub/core/diff/repository/repository.py,sha256=x3QP9VmBVYBOVtf3IZUyzXqCd8sSfmHTqVoYlAOdGao,26006
@@ -206,7 +206,7 @@ infrahub/core/node/__init__.py,sha256=4of1tA26-EV8FOubg-rhBFU9wkJ0BBipePnnvx1vi_
206
206
  infrahub/core/node/base.py,sha256=BAowVRCK_WC50yXym1kCyUppJDJnrODGU5uoj1s0Yd4,2564
207
207
  infrahub/core/node/constraints/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
208
208
  infrahub/core/node/constraints/attribute_uniqueness.py,sha256=9MThTmuqZ7RgK71ZZARlw1k1x3ARn1U67g2_Gatd6rE,2099
209
- infrahub/core/node/constraints/grouped_uniqueness.py,sha256=GQ1-l4ZoZR6FoklHAdqCaNwX3TmW6qrvKYJEVtdPOfc,12056
209
+ infrahub/core/node/constraints/grouped_uniqueness.py,sha256=F5pmnXVuQNlVmdZY5FRxSGK4gGi1BK1IRgw4emCTlLw,9506
210
210
  infrahub/core/node/constraints/interface.py,sha256=fwB32pRLxteQyKRArqekQ0RXlrDkyzp7Vmq03vSpUEo,291
211
211
  infrahub/core/node/create.py,sha256=1mAFaMLqRmuONIwL549JQLFbOpEbP3rBQEb1D2VArcc,7752
212
212
  infrahub/core/node/delete_validator.py,sha256=mj_HQXkTeP_A3po65-R5bCJnDM9CmFFmcUQIxwPlofc,10559
@@ -334,8 +334,8 @@ infrahub/core/validators/tasks.py,sha256=oaV1rOFiGOkbZYjpK2d5UTj_LFJAghSMKbO7w5f
334
334
  infrahub/core/validators/uniqueness/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
335
335
  infrahub/core/validators/uniqueness/checker.py,sha256=RpiLpIjbdkwwjivry-vjEkVim6ZoC-t2H5Bal7ngASQ,10375
336
336
  infrahub/core/validators/uniqueness/index.py,sha256=Jw1o-UVinQquNduZ5vCCzt8GUfIEdVzBo-1XyRti8F8,5068
337
- infrahub/core/validators/uniqueness/model.py,sha256=V2aejcuHPhgC5nTrS7xX0JFMzprVu90QAau-rUzruCY,5135
338
- infrahub/core/validators/uniqueness/query.py,sha256=5HRjt4WlQCIP9krlKqkBNMJGgavKik8-Z11Q1_YllLk,11650
337
+ infrahub/core/validators/uniqueness/model.py,sha256=3MXrE9lLMI1B2TXxMS2Eb59uOI_Q4havuluoI8fy4EE,5593
338
+ infrahub/core/validators/uniqueness/query.py,sha256=3qJfoxwau3303BCkjJwPngotQYjTRuewqTQvcPzsICE,20103
339
339
  infrahub/database/__init__.py,sha256=sRME_Cm74b5MsKyqMLRpAv3E38Vw2H1yv20JjSBb-Vg,20836
340
340
  infrahub/database/index.py,sha256=ATLqw9Grqbq7haGGm14VSEPmcPniid--YATiffo4sA0,1676
341
341
  infrahub/database/memgraph.py,sha256=Fg3xHP9s0MiBBmMvcEmsJvuIUSq8U_XCS362HDE9d1s,1742
@@ -471,7 +471,7 @@ infrahub/graphql/mutations/diff_conflict.py,sha256=JngQfyKXCVlmtlqQ_VyabmrOEDOEK
471
471
  infrahub/graphql/mutations/generator.py,sha256=Ulw4whZm8Gc8lJjwfUFoFSsR0cOUliFKl87Oca4B9O0,3579
472
472
  infrahub/graphql/mutations/graphql_query.py,sha256=mp_O2byChneCihUrEAFEiIAgJ1gW9WrgtwPetUQmkJw,3562
473
473
  infrahub/graphql/mutations/ipam.py,sha256=wIN8OcTNCHVy32YgatWZi2Of-snFYBd4wlxOAJvE-AY,15961
474
- infrahub/graphql/mutations/main.py,sha256=I4qE7mf-vQcDqPdW_8MsyRe_GtuPeHJj-pjB3_tzsPk,19716
474
+ infrahub/graphql/mutations/main.py,sha256=GgETTp-KKyVTnvD8GO7hp1oWIq-eOJ7E_WL-wXUP5Us,20392
475
475
  infrahub/graphql/mutations/menu.py,sha256=u2UbOA-TFDRcZRGFkgYTmpGxN2IAUgOvJXd7SnsufyI,3708
476
476
  infrahub/graphql/mutations/models.py,sha256=ilkSLr8OxVO9v3Ra_uDyUTJT9qPOmdPMqQbuWIydJMo,264
477
477
  infrahub/graphql/mutations/node_getter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -719,10 +719,10 @@ infrahub_sdk/jinja2.py,sha256=lTfV9E_P5gApaX6RW9M8U8oixQi-0H3U8wcs8fdGVaU,1150
719
719
  infrahub_sdk/node/__init__.py,sha256=clAUZ9lNVPFguelR5Sg9PzklAZruTKEm2xk-BaO68l8,1262
720
720
  infrahub_sdk/node/attribute.py,sha256=oEY1qxip8ETEx9Q33NhSQo013zmzrmpVIFzSkEMUY8M,4547
721
721
  infrahub_sdk/node/constants.py,sha256=TJO4uxvv7sc3FjoLdQdV7Ccymqz8AqxDenARst8awb4,775
722
- infrahub_sdk/node/node.py,sha256=ht6T2Td9CSRrdgCTIemVJmTqEuL4a9YC77zhElN8S0U,70138
722
+ infrahub_sdk/node/node.py,sha256=34rIDO7ZdA8uJpsuBbPDDZ5GSUKkL1nlnFBvoM5bDRo,70674
723
723
  infrahub_sdk/node/parsers.py,sha256=sLDdT6neoYSZIjOCmq8Bgd0LK8FFoasjvJLuSz0whSU,543
724
724
  infrahub_sdk/node/property.py,sha256=8Mjkc8bp3kLlHyllwxDJlpJTuOA1ciMgY8mtH3dFVLM,728
725
- infrahub_sdk/node/related_node.py,sha256=41VTj4k1qojuyBZr0XiD7e2NESl8YwsU3fCmaarlrD0,9916
725
+ infrahub_sdk/node/related_node.py,sha256=fPMnZ83OZnnbimaPC14MdE3lR-kumAA6hbOhRlo1gms,10093
726
726
  infrahub_sdk/node/relationship.py,sha256=ax9BfYFEfzvUmVxiC1RrhtnpV9ZPuuvQFN_DNRGUHLU,11911
727
727
  infrahub_sdk/object_store.py,sha256=d-EDnxPpw_7BsbjbGbH50rjt-1-Ojj2zNrhFansP5hA,4299
728
728
  infrahub_sdk/operation.py,sha256=hsbZSjLbLsqvjZg5R5x_bOxxlteXJAk0fQy3mLrZhn4,2730
@@ -801,8 +801,8 @@ infrahub_testcontainers/models.py,sha256=ASYyvl7d_WQz_i7y8-3iab9hwwmCl3OCJavqVbe
801
801
  infrahub_testcontainers/performance_test.py,sha256=hvwiy6tc_lWniYqGkqfOXVGAmA_IV15VOZqbiD9ezno,6149
802
802
  infrahub_testcontainers/plugin.py,sha256=I3RuZQ0dARyKHuqCf0y1Yj731P2Mwf3BJUehRJKeWrs,5645
803
803
  infrahub_testcontainers/prometheus.yml,sha256=610xQEyj3xuVJMzPkC4m1fRnCrjGpiRBrXA2ytCLa54,599
804
- infrahub_server-1.3.1.dist-info/LICENSE.txt,sha256=TfPDBt3ar0uv_f9cqCDMZ5rIzW3CY8anRRd4PkL6ejs,34522
805
- infrahub_server-1.3.1.dist-info/METADATA,sha256=7WxSk_ctoonbM_snztUovCRsd9Lq-grShqcalwFQuEo,8205
806
- infrahub_server-1.3.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
807
- infrahub_server-1.3.1.dist-info/entry_points.txt,sha256=UXIeFWDsrV-4IllNvUEd6KieYGzQfn9paga2YyABOQI,393
808
- infrahub_server-1.3.1.dist-info/RECORD,,
804
+ infrahub_server-1.3.2.dist-info/LICENSE.txt,sha256=TfPDBt3ar0uv_f9cqCDMZ5rIzW3CY8anRRd4PkL6ejs,34522
805
+ infrahub_server-1.3.2.dist-info/METADATA,sha256=ryVIpJ8RWv_Brr02affO2F-ZGjjAuz0ahV-OGLaIr7U,8205
806
+ infrahub_server-1.3.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
807
+ infrahub_server-1.3.2.dist-info/entry_points.txt,sha256=UXIeFWDsrV-4IllNvUEd6KieYGzQfn9paga2YyABOQI,393
808
+ infrahub_server-1.3.2.dist-info/RECORD,,