infrahub-server 1.1.5__py3-none-any.whl → 1.1.7__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 (66) hide show
  1. infrahub/api/oidc.py +1 -0
  2. infrahub/core/attribute.py +4 -1
  3. infrahub/core/branch/tasks.py +7 -4
  4. infrahub/core/diff/calculator.py +21 -39
  5. infrahub/core/diff/combiner.py +11 -7
  6. infrahub/core/diff/coordinator.py +49 -70
  7. infrahub/core/diff/data_check_synchronizer.py +86 -7
  8. infrahub/core/diff/enricher/aggregated.py +3 -3
  9. infrahub/core/diff/enricher/cardinality_one.py +1 -6
  10. infrahub/core/diff/enricher/labels.py +13 -3
  11. infrahub/core/diff/enricher/path_identifier.py +2 -8
  12. infrahub/core/diff/ipam_diff_parser.py +1 -1
  13. infrahub/core/diff/merger/merger.py +5 -3
  14. infrahub/core/diff/merger/serializer.py +15 -8
  15. infrahub/core/diff/model/path.py +42 -24
  16. infrahub/core/diff/query/all_conflicts.py +5 -2
  17. infrahub/core/diff/query/diff_get.py +19 -23
  18. infrahub/core/diff/query/field_specifiers.py +2 -0
  19. infrahub/core/diff/query/field_summary.py +2 -1
  20. infrahub/core/diff/query/filters.py +12 -1
  21. infrahub/core/diff/query/has_conflicts_query.py +5 -2
  22. infrahub/core/diff/query/{drop_tracking_id.py → merge_tracking_id.py} +3 -3
  23. infrahub/core/diff/query/roots_metadata.py +8 -1
  24. infrahub/core/diff/query/save.py +148 -63
  25. infrahub/core/diff/query/summary_counts_enricher.py +220 -0
  26. infrahub/core/diff/query/time_range_query.py +2 -1
  27. infrahub/core/diff/query_parser.py +49 -24
  28. infrahub/core/diff/repository/deserializer.py +74 -71
  29. infrahub/core/diff/repository/repository.py +119 -30
  30. infrahub/core/node/__init__.py +6 -1
  31. infrahub/core/node/constraints/grouped_uniqueness.py +9 -2
  32. infrahub/core/node/ipam.py +6 -1
  33. infrahub/core/node/permissions.py +4 -0
  34. infrahub/core/query/diff.py +223 -230
  35. infrahub/core/query/node.py +8 -2
  36. infrahub/core/query/relationship.py +2 -1
  37. infrahub/core/query/resource_manager.py +3 -1
  38. infrahub/core/relationship/model.py +1 -1
  39. infrahub/core/schema/schema_branch.py +16 -7
  40. infrahub/core/utils.py +1 -0
  41. infrahub/core/validators/uniqueness/query.py +20 -17
  42. infrahub/database/__init__.py +13 -0
  43. infrahub/dependencies/builder/constraint/grouped/node_runner.py +0 -2
  44. infrahub/dependencies/builder/diff/coordinator.py +0 -2
  45. infrahub/git/integrator.py +10 -6
  46. infrahub/graphql/mutations/computed_attribute.py +3 -1
  47. infrahub/graphql/mutations/diff.py +28 -4
  48. infrahub/graphql/mutations/main.py +11 -6
  49. infrahub/graphql/mutations/relationship.py +29 -1
  50. infrahub/graphql/mutations/tasks.py +6 -3
  51. infrahub/graphql/queries/resource_manager.py +7 -3
  52. infrahub/permissions/__init__.py +2 -1
  53. infrahub/permissions/types.py +26 -0
  54. infrahub/proposed_change/tasks.py +6 -1
  55. infrahub/storage.py +6 -5
  56. {infrahub_server-1.1.5.dist-info → infrahub_server-1.1.7.dist-info}/METADATA +41 -7
  57. {infrahub_server-1.1.5.dist-info → infrahub_server-1.1.7.dist-info}/RECORD +64 -64
  58. infrahub_testcontainers/container.py +12 -3
  59. infrahub_testcontainers/docker-compose.test.yml +22 -3
  60. infrahub_testcontainers/haproxy.cfg +43 -0
  61. infrahub_testcontainers/helpers.py +85 -1
  62. infrahub/core/diff/enricher/summary_counts.py +0 -105
  63. infrahub/dependencies/builder/diff/enricher/summary_counts.py +0 -8
  64. {infrahub_server-1.1.5.dist-info → infrahub_server-1.1.7.dist-info}/LICENSE.txt +0 -0
  65. {infrahub_server-1.1.5.dist-info → infrahub_server-1.1.7.dist-info}/WHEEL +0 -0
  66. {infrahub_server-1.1.5.dist-info → infrahub_server-1.1.7.dist-info}/entry_points.txt +0 -0
@@ -633,6 +633,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
633
633
  related_node_ids: set | None = None,
634
634
  filter_sensitive: bool = False,
635
635
  permissions: dict | None = None,
636
+ include_properties: bool = True,
636
637
  ) -> dict:
637
638
  """Generate GraphQL Payload for all attributes
638
639
 
@@ -686,10 +687,14 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
686
687
  related_node_ids=related_node_ids,
687
688
  filter_sensitive=filter_sensitive,
688
689
  permissions=permissions,
690
+ include_properties=include_properties,
689
691
  )
690
692
  else:
691
693
  response[field_name] = await field.to_graphql(
692
- db=db, filter_sensitive=filter_sensitive, permissions=permissions
694
+ db=db,
695
+ filter_sensitive=filter_sensitive,
696
+ permissions=permissions,
697
+ include_properties=include_properties,
693
698
  )
694
699
 
695
700
  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:
@@ -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:
@@ -98,221 +98,6 @@ class DiffCountChanges(Query):
98
98
  return branch_count_map
99
99
 
100
100
 
101
- class DiffAllPathsQuery(DiffQuery):
102
- """Gets the required Cypher paths for a diff"""
103
-
104
- name = "diff_node"
105
- type = QueryType.READ
106
-
107
- def __init__(
108
- self,
109
- base_branch: Branch,
110
- diff_branch_from_time: Timestamp,
111
- current_node_field_specifiers: list[tuple[str, str]] | None = None,
112
- new_node_field_specifiers: list[tuple[str, str]] | None = None,
113
- **kwargs: Any,
114
- ):
115
- self.base_branch = base_branch
116
- self.diff_branch_from_time = diff_branch_from_time
117
- self.current_node_field_specifiers = current_node_field_specifiers
118
- self.new_node_field_specifiers = new_node_field_specifiers
119
-
120
- super().__init__(**kwargs)
121
-
122
- async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None:
123
- from_str = self.diff_from.to_string()
124
- self.params.update(
125
- {
126
- "base_branch_name": self.base_branch.name,
127
- "branch_name": self.branch.name,
128
- "global_branch_name": GLOBAL_BRANCH_NAME,
129
- "branch_from_time": self.diff_branch_from_time.to_string(),
130
- "from_time": from_str,
131
- "to_time": self.diff_to.to_string(),
132
- "branch_local": BranchSupportType.LOCAL.value,
133
- "branch_aware": BranchSupportType.AWARE.value,
134
- "branch_agnostic": BranchSupportType.AGNOSTIC.value,
135
- "new_node_field_specifiers": self.new_node_field_specifiers,
136
- "current_node_field_specifiers": self.current_node_field_specifiers,
137
- }
138
- )
139
- query = """
140
- WITH CASE
141
- WHEN $new_node_field_specifiers IS NULL AND $current_node_field_specifiers IS NULL THEN [[NULL, $from_time]]
142
- WHEN $new_node_field_specifiers IS NULL OR size($new_node_field_specifiers) = 0 THEN [[$current_node_field_specifiers, $from_time]]
143
- WHEN $current_node_field_specifiers IS NULL OR size($current_node_field_specifiers) = 0 THEN [[$new_node_field_specifiers, $branch_from_time]]
144
- ELSE [[$new_node_field_specifiers, $branch_from_time], [$current_node_field_specifiers, $from_time]]
145
- END AS diff_filter_params_list
146
- UNWIND diff_filter_params_list AS diff_filter_params
147
- WITH diff_filter_params[0] AS node_field_specifiers_list, diff_filter_params[1] AS from_time
148
- CALL {
149
- // -------------------------------------
150
- // These lists contain duplicate data, but vastly improve querying speed below
151
- // -------------------------------------
152
- WITH node_field_specifiers_list
153
- UNWIND node_field_specifiers_list AS nfs
154
- WITH nfs[0] AS uuid, nfs[1] AS field_name
155
- WITH collect(DISTINCT uuid) as uuids, collect(DISTINCT field_name) AS field_names
156
- RETURN uuids AS node_ids_list, field_names AS field_names_list
157
- }
158
- CALL {
159
- WITH node_field_specifiers_list, node_ids_list, field_names_list, from_time
160
- CALL {
161
- WITH node_field_specifiers_list, node_ids_list, field_names_list, from_time
162
- // -------------------------------------
163
- // Identify properties added/removed on branch
164
- // -------------------------------------
165
- MATCH diff_rel_path = (root:Root)<-[r_root:IS_PART_OF]-(n:Node)-[r_node]-(p)-[diff_rel {branch: $branch_name}]->(q)
166
- WHERE (
167
- (from_time <= diff_rel.from < $to_time)
168
- OR (from_time <= diff_rel.to < $to_time)
169
- )
170
- AND (size(node_ids_list) = 0 OR n.uuid IN node_ids_list)
171
- AND (size(field_names_list) = 0 OR p.name IN field_names_list)
172
- // exclude attributes and relationships under added/removed nodes, attrs, and rels b/c they are covered above
173
- AND ALL(
174
- r in [r_root, r_node]
175
- WHERE r.from <= from_time AND r.branch IN [$branch_name, $base_branch_name]
176
- )
177
- AND p.branch_support = $branch_aware
178
- AND any(l in labels(p) WHERE l in ["Attribute", "Relationship"])
179
- AND type(diff_rel) IN ["IS_VISIBLE", "IS_PROTECTED", "HAS_SOURCE", "HAS_OWNER", "HAS_VALUE"]
180
- AND any(l in labels(q) WHERE l in ["Boolean", "Node", "AttributeValue"])
181
- AND type(r_node) IN ["HAS_ATTRIBUTE", "IS_RELATED"]
182
- AND (node_field_specifiers_list IS NULL OR [n.uuid, p.name] IN node_field_specifiers_list)
183
- AND ALL(
184
- r_pair IN [[r_root, r_node], [r_node, diff_rel]]
185
- // filter out paths where a base branch edge follows a branch edge
186
- WHERE ((r_pair[0]).branch = $base_branch_name OR (r_pair[1]).branch = $branch_name)
187
- // filter out paths where an active edge follows a deleted edge
188
- AND ((r_pair[0]).status = "active" OR (r_pair[1]).status = "deleted")
189
- // filter out paths where an earlier from time follows a later from time
190
- AND (r_pair[0]).from <= (r_pair[1]).from
191
- // require adjacent edge pairs to have overlapping times, but only if on the same branch
192
- AND (
193
- (r_pair[0]).branch <> (r_pair[1]).branch
194
- OR (r_pair[0]).to IS NULL
195
- OR (r_pair[0]).to >= (r_pair[1]).from
196
- )
197
- )
198
- AND [%(id_func)s(n), type(r_node)] <> [%(id_func)s(q), type(diff_rel)]
199
- WITH diff_rel_path, r_root, n, r_node, p, diff_rel, from_time
200
- ORDER BY
201
- %(id_func)s(n) DESC,
202
- %(id_func)s(p) DESC,
203
- type(diff_rel),
204
- r_node.branch = diff_rel.branch DESC,
205
- r_root.branch = diff_rel.branch DESC,
206
- diff_rel.from DESC,
207
- r_node.from DESC,
208
- r_root.from DESC
209
- WITH n, p, from_time, diff_rel, diff_rel_path
210
- CALL {
211
- // -------------------------------------
212
- // Exclude properties under nodes and attributes/relationship deleted
213
- // on this branch in the timeframe because those were all handled above
214
- // -------------------------------------
215
- WITH n, p, from_time
216
- CALL {
217
- WITH n, from_time
218
- OPTIONAL MATCH (root:Root)<-[r_root_deleted:IS_PART_OF {branch: $branch_name}]-(n)
219
- WHERE from_time <= r_root_deleted.from < $to_time
220
- WITH r_root_deleted
221
- ORDER BY r_root_deleted.status DESC
222
- LIMIT 1
223
- RETURN COALESCE(r_root_deleted.status = "deleted", FALSE) AS node_deleted
224
- }
225
- WITH n, p, from_time, node_deleted
226
- CALL {
227
- WITH n, p, from_time
228
- OPTIONAL MATCH (n)-[r_node_deleted {branch: $branch_name}]-(p)
229
- WHERE from_time <= r_node_deleted.from < $to_time
230
- AND type(r_node_deleted) IN ["HAS_ATTRIBUTE", "IS_RELATED"]
231
- WITH r_node_deleted
232
- ORDER BY r_node_deleted.status DESC
233
- LIMIT 1
234
- RETURN COALESCE(r_node_deleted.status = "deleted", FALSE) AS field_deleted
235
- }
236
- RETURN node_deleted OR field_deleted AS node_or_field_deleted
237
- }
238
- WITH n, p, diff_rel, diff_rel_path, node_or_field_deleted
239
- WHERE node_or_field_deleted = FALSE
240
- WITH n, p, type(diff_rel) AS drt, head(collect(diff_rel_path)) AS deepest_diff_path
241
- RETURN deepest_diff_path
242
- }
243
- RETURN deepest_diff_path AS diff_path
244
- }
245
- WITH DISTINCT diff_path AS diff_path
246
- CALL {
247
- WITH diff_path
248
- WITH diff_path, nodes(diff_path) AS d_nodes, relationships(diff_path) AS d_rels
249
- WITH diff_path, d_rels[0] AS r_root, d_nodes[1] AS n, d_rels[1] AS r_node, d_nodes[2] AS attr_rel, d_rels[2] AS r_prop
250
- // -------------------------------------
251
- // add base branch paths before branched_from, if they exist
252
- // -------------------------------------
253
- WITH n, attr_rel, r_node, r_prop
254
- OPTIONAL MATCH latest_base_path = (:Root)<-[base_r_root:IS_PART_OF {branch: $base_branch_name}]
255
- -(n)-[base_r_node {branch: $base_branch_name}]
256
- -(attr_rel)-[base_r_prop {branch: $base_branch_name}]->(base_prop)
257
- WHERE type(base_r_node) = type(r_node)
258
- AND type(base_r_prop) = type(r_prop)
259
- AND [%(id_func)s(n), type(base_r_node)] <> [%(id_func)s(base_prop), type(base_r_prop)]
260
- AND all(
261
- r in relationships(latest_base_path)
262
- WHERE r.from < $branch_from_time
263
- )
264
- WITH latest_base_path, base_r_root, base_r_node, base_r_prop
265
- ORDER BY base_r_prop.from DESC, base_r_node.from DESC, base_r_root.from DESC
266
- LIMIT 1
267
- RETURN latest_base_path
268
- }
269
- WITH diff_path, latest_base_path
270
- UNWIND [diff_path, latest_base_path] AS penultimate_path
271
- WITH penultimate_path
272
- CALL {
273
- WITH penultimate_path
274
- WITH penultimate_path, nodes(penultimate_path) AS d_nodes, relationships(penultimate_path) AS d_rels
275
- WITH penultimate_path, d_rels[0] AS r_root, d_nodes[1] AS n, d_rels[1] AS r_node, d_nodes[2] AS attr_rel, d_rels[2] AS r_prop
276
- // -------------------------------------
277
- // Add peer-side of any relationships to get the peer's ID
278
- // -------------------------------------
279
- WITH r_root, n, r_node, attr_rel, r_prop
280
- OPTIONAL MATCH peer_path = (
281
- (:Root)<-[peer_r_root:IS_PART_OF]-(n)-[peer_r_node:IS_RELATED]-(attr_rel:Relationship)-[r_peer:IS_RELATED]-(peer:Node)
282
- )
283
- WHERE type(r_prop) <> "IS_RELATED"
284
- AND %(id_func)s(peer_r_root) = %(id_func)s(r_root)
285
- AND %(id_func)s(peer_r_node) = %(id_func)s(r_node)
286
- AND [%(id_func)s(n), type(peer_r_node)] <> [%(id_func)s(peer), type(r_peer)]
287
- AND r_peer.from < $to_time
288
- // filter out paths where an earlier from time follows a later from time
289
- AND peer_r_node.from <= r_peer.from
290
- // filter out paths where a base branch edge follows a branch edge
291
- AND (peer_r_node.branch = $base_branch_name OR r_peer.branch = $branch_name)
292
- // filter out paths where an active edge follows a deleted edge
293
- AND (peer_r_node.status = "active" OR r_peer.status = "deleted")
294
- // require adjacent edge pairs to have overlapping times, but only if on the same branch
295
- AND (
296
- peer_r_node.branch <> r_peer.branch
297
- OR peer_r_node.to IS NULL
298
- OR peer_r_node.to >= r_peer.from
299
- )
300
- WITH peer_path, r_peer, r_prop
301
- ORDER BY r_peer.branch = r_prop.branch DESC, r_peer.from DESC
302
- LIMIT 1
303
- RETURN peer_path
304
- }
305
- WITH penultimate_path, peer_path
306
- WITH reduce(
307
- diff_rel_paths = [], item IN [penultimate_path, peer_path] |
308
- CASE WHEN item IS NULL THEN diff_rel_paths ELSE diff_rel_paths + [item] END
309
- ) AS diff_rel_paths
310
- UNWIND diff_rel_paths AS diff_path
311
- """ % {"id_func": db.get_id_function_name()}
312
- self.add_to_query(query)
313
- self.return_labels = ["DISTINCT diff_path AS diff_path"]
314
-
315
-
316
101
  class DiffCalculationQuery(DiffQuery):
317
102
  type = QueryType.READ
318
103
  insert_limit = False
@@ -352,6 +137,14 @@ CALL {
352
137
  r in relationships(latest_base_path)
353
138
  WHERE r.from < $branch_from_time
354
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
+ )
355
148
  WITH latest_base_path, base_r_root, base_r_node, base_r_prop
356
149
  ORDER BY base_r_prop.from DESC, base_r_node.from DESC, base_r_root.from DESC
357
150
  LIMIT 1
@@ -361,7 +154,7 @@ CALL {
361
154
  relationship_peer_side_query = """
362
155
  WITH diff_path, latest_base_path, has_more_data
363
156
  UNWIND [diff_path, latest_base_path] AS penultimate_path
364
- WITH penultimate_path, has_more_data
157
+ WITH DISTINCT penultimate_path, has_more_data
365
158
  CALL {
366
159
  WITH penultimate_path
367
160
  WITH penultimate_path, nodes(penultimate_path) AS d_nodes, relationships(penultimate_path) AS d_rels
@@ -390,8 +183,13 @@ CALL {
390
183
  OR peer_r_node.to IS NULL
391
184
  OR peer_r_node.to >= r_peer.from
392
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)
393
191
  WITH peer_path, r_peer, r_prop
394
- 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
395
193
  LIMIT 1
396
194
  RETURN peer_path
397
195
  }
@@ -456,21 +254,20 @@ AND (
456
254
  (
457
255
  ($new_node_ids_list IS NOT NULL AND p.uuid IN $new_node_ids_list)
458
256
  AND (
459
- ($branch_from_time <= diff_rel.from < $to_time)
257
+ ($branch_from_time <= diff_rel.from < $to_time AND (diff_rel.to IS NULL OR diff_rel.to > $to_time))
460
258
  OR ($branch_from_time <= diff_rel.to < $to_time)
461
259
  )
462
260
  )
463
261
  OR (
464
- ($current_node_ids_list IS NOT NULL AND p.uuid IN $current_node_ids_list)
262
+ (
263
+ ($current_node_ids_list IS NOT NULL AND p.uuid IN $current_node_ids_list)
264
+ OR ($current_node_ids_list IS NULL AND $new_node_ids_list IS NULL)
265
+ )
465
266
  AND (
466
- ($from_time <= diff_rel.from < $to_time)
267
+ ($from_time <= diff_rel.from < $to_time AND (diff_rel.to IS NULL OR diff_rel.to > $to_time))
467
268
  OR ($from_time <= diff_rel.to < $to_time)
468
269
  )
469
270
  )
470
- OR (
471
- ($from_time <= diff_rel.from < $to_time)
472
- OR ($from_time <= diff_rel.to < $to_time)
473
- )
474
271
  )
475
272
  // -------------------------------------
476
273
  // Limit the number of nodes
@@ -525,6 +322,14 @@ CALL {
525
322
  AND [%(id_func)s(p), type(r_node)] <> [%(id_func)s(prop), type(r_prop)]
526
323
  AND top_diff_rel.status = r_node.status
527
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
+ )
528
333
  WITH path, p, node, prop, r_prop, r_node, type(r_node) AS rel_type, row_from_time
529
334
  // -------------------------------------
530
335
  // Exclude attributes/relationships added then removed on branch within timeframe
@@ -616,22 +421,26 @@ AND (
616
421
  OR ($current_node_field_specifiers_map IS NULL AND $new_node_field_specifiers_map IS NULL)
617
422
  )
618
423
  AND (r_root.from < $from_time OR p.branch_support = $branch_agnostic)
619
- AND ($from_time <= diff_rel.from < $to_time)
620
- AND (diff_rel.to IS NULL OR ($from_time <= diff_rel.to < $to_time))
424
+ AND (
425
+ ($from_time <= diff_rel.from < $to_time AND (diff_rel.to IS NULL OR diff_rel.to > $to_time))
426
+ OR ($from_time <= diff_rel.to < $to_time)
427
+ )
621
428
  )
622
429
  // time-based filters for new nodes
623
430
  OR (
624
431
  ($new_node_field_specifiers_map IS NOT NULL AND q.name IN $new_node_field_specifiers_map[p.uuid])
625
432
  AND (r_root.from < $branch_from_time OR p.branch_support = $branch_agnostic)
626
- AND ($branch_from_time <= diff_rel.from < $to_time)
627
- AND (diff_rel.to IS NULL OR ($branch_from_time <= diff_rel.to < $to_time))
433
+ AND (
434
+ ($branch_from_time <= diff_rel.from < $to_time AND (diff_rel.to IS NULL OR diff_rel.to > $to_time))
435
+ OR ($branch_from_time <= diff_rel.to < $to_time)
436
+ )
628
437
  )
629
438
  )
630
439
  // -------------------------------------
631
440
  // Limit the number of paths
632
441
  // -------------------------------------
633
442
  WITH root, r_root, p, diff_rel, q
634
- ORDER BY p.uuid, q.uuid, diff_rel.branch, diff_rel.from
443
+ ORDER BY r_root.from, p.uuid, q.uuid, diff_rel.branch, diff_rel.from
635
444
  SKIP $offset
636
445
  LIMIT $limit
637
446
  // -------------------------------------
@@ -695,17 +504,26 @@ CALL {
695
504
  AND [%(id_func)s(p), type(mid_diff_rel)] <> [%(id_func)s(prop), type(r_prop)]
696
505
  // exclude paths where an active edge is below a deleted edge
697
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
+ )
698
515
  WITH path, prop, r_prop, mid_r_root
699
516
  ORDER BY
700
517
  type(r_prop),
701
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,
702
520
  r_prop.from DESC,
703
521
  mid_r_root.from DESC
704
522
  WITH prop, type(r_prop) AS type_r_prop, head(collect(path)) AS latest_prop_path
705
523
  RETURN latest_prop_path
706
524
  }
707
525
  // -------------------------------------
708
- // Exclude properties within the timeframe
526
+ // Exclude properties added and deleted within the timeframe
709
527
  // -------------------------------------
710
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
711
529
  CALL {
@@ -725,3 +543,178 @@ WHERE intra_branch_update = FALSE
725
543
  self.add_to_query(self.get_relationship_peer_side_query(db=db))
726
544
  self.add_to_query("UNWIND diff_rel_paths AS diff_path")
727
545
  self.return_labels = ["DISTINCT diff_path AS diff_path", "has_more_data"]
546
+
547
+
548
+ class DiffPropertyPathsQuery(DiffCalculationQuery):
549
+ name = "diff_property_paths"
550
+
551
+ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None:
552
+ params_dict = self.get_params()
553
+ self.params.update(params_dict)
554
+
555
+ self.params.update(
556
+ {
557
+ "current_node_field_specifiers_map": {
558
+ node_uuid: list(field_names)
559
+ for node_uuid, field_names in self.current_node_field_specifiers.items()
560
+ }
561
+ if self.current_node_field_specifiers is not None
562
+ else None,
563
+ "new_node_field_specifiers_map": {
564
+ node_uuid: list(field_names) for node_uuid, field_names in self.new_node_field_specifiers.items()
565
+ }
566
+ if self.new_node_field_specifiers is not None
567
+ else None,
568
+ }
569
+ )
570
+ properties_path_query = """
571
+ // -------------------------------------
572
+ // Identify properties added/removed on branch
573
+ // -------------------------------------
574
+ MATCH diff_rel_path = (root:Root)<-[r_root:IS_PART_OF]-(n:Node)-[r_node]-(p)-[diff_rel {branch: $branch_name}]->(q)
575
+ WHERE p.branch_support = $branch_aware
576
+ AND any(l in labels(p) WHERE l in ["Attribute", "Relationship"])
577
+ AND type(diff_rel) IN ["IS_VISIBLE", "IS_PROTECTED", "HAS_SOURCE", "HAS_OWNER", "HAS_VALUE"]
578
+ AND any(l in labels(q) WHERE l in ["Boolean", "Node", "AttributeValue"])
579
+ AND type(r_node) IN ["HAS_ATTRIBUTE", "IS_RELATED"]
580
+ // node ID and field name filtering first pass
581
+ AND (
582
+ (
583
+ $current_node_field_specifiers_map IS NOT NULL
584
+ AND $current_node_field_specifiers_map[n.uuid] IS NOT NULL
585
+ AND p.name IN $current_node_field_specifiers_map[n.uuid]
586
+ ) OR (
587
+ $new_node_field_specifiers_map IS NOT NULL
588
+ AND $new_node_field_specifiers_map[n.uuid] IS NOT NULL
589
+ AND p.name IN $new_node_field_specifiers_map[n.uuid]
590
+ ) OR (
591
+ $current_node_field_specifiers_map IS NULL
592
+ AND $new_node_field_specifiers_map IS NULL
593
+ )
594
+ )
595
+ // node ID and field name filtering second pass
596
+ AND (
597
+ // time-based filters for nodes already included in the diff or fresh changes
598
+ (
599
+ (
600
+ ($current_node_field_specifiers_map IS NOT NULL AND p.name IN $current_node_field_specifiers_map[n.uuid])
601
+ OR ($current_node_field_specifiers_map IS NULL AND $new_node_field_specifiers_map IS NULL)
602
+ )
603
+ AND (
604
+ ($from_time <= diff_rel.from < $to_time AND (diff_rel.to IS NULL OR diff_rel.to > $to_time))
605
+ OR ($from_time <= diff_rel.to < $to_time)
606
+ )
607
+ // skip paths where nodes/attrs/rels are updated after $from_time, those are handled in other queries
608
+ AND (
609
+ r_root.from <= $from_time AND (r_root.to IS NULL OR r_root.branch <> diff_rel.branch OR r_root.to <= $from_time)
610
+ AND r_node.from <= $from_time AND (r_node.to IS NULL OR r_node.branch <> diff_rel.branch OR r_node.to <= $from_time)
611
+ )
612
+ )
613
+ // time-based filters for new nodes
614
+ OR (
615
+ ($new_node_field_specifiers_map IS NOT NULL AND p.name IN $new_node_field_specifiers_map[n.uuid])
616
+ AND (
617
+ ($branch_from_time <= diff_rel.from < $to_time AND (diff_rel.to IS NULL OR diff_rel.to > $to_time))
618
+ OR ($branch_from_time <= diff_rel.to < $to_time)
619
+ )
620
+ // skip paths where nodes/attrs/rels are updated after $branch_from_time, those are handled in other queries
621
+ AND (
622
+ r_root.from <= $branch_from_time AND (r_root.to IS NULL OR r_root.branch <> diff_rel.branch OR r_root.to <= $branch_from_time)
623
+ AND r_node.from <= $branch_from_time AND (r_node.to IS NULL OR r_node.branch <> diff_rel.branch OR r_node.to <= $branch_from_time)
624
+ )
625
+ )
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
+ )
635
+ AND ALL(
636
+ r_pair IN [[r_root, r_node], [r_node, diff_rel]]
637
+ // filter out paths where a base branch edge follows a branch edge
638
+ WHERE ((r_pair[0]).branch = $base_branch_name OR (r_pair[1]).branch = $branch_name)
639
+ // filter out paths where an active edge follows a deleted edge
640
+ AND ((r_pair[0]).status = "active" OR (r_pair[1]).status = "deleted")
641
+ // filter out paths where an earlier from time follows a later from time
642
+ AND (r_pair[0]).from <= (r_pair[1]).from
643
+ // require adjacent edge pairs to have overlapping times, but only if on the same branch
644
+ AND (
645
+ (r_pair[0]).branch <> (r_pair[1]).branch
646
+ OR (r_pair[0]).to IS NULL
647
+ OR (r_pair[0]).to >= (r_pair[1]).from
648
+ )
649
+ )
650
+ AND [%(id_func)s(n), type(r_node)] <> [%(id_func)s(q), type(diff_rel)]
651
+ // -------------------------------------
652
+ // Limit the number of paths
653
+ // -------------------------------------
654
+ WITH diff_rel_path, r_root, n, r_node, p, diff_rel
655
+ ORDER BY r_root.from, n.uuid, p.uuid, type(diff_rel), diff_rel.branch, diff_rel.from
656
+ SKIP $offset
657
+ LIMIT $limit
658
+ // -------------------------------------
659
+ // Add flag to indicate if there is more data after this
660
+ // -------------------------------------
661
+ WITH collect([diff_rel_path, r_root, n, r_node, p, diff_rel]) AS limited_results
662
+ WITH limited_results, size(limited_results) = $limit AS has_more_data
663
+ UNWIND limited_results AS one_result
664
+ WITH one_result[0] AS diff_rel_path, one_result[1] AS r_root, one_result[2] AS n,
665
+ one_result[3] AS r_node, one_result[4] AS p, one_result[5] AS diff_rel, has_more_data
666
+ // -------------------------------------
667
+ // Add correct from_time for row
668
+ // -------------------------------------
669
+ WITH diff_rel_path, r_root, n, r_node, p, diff_rel, has_more_data, CASE
670
+ WHEN $new_node_field_specifiers_map IS NOT NULL AND p.name IN $new_node_field_specifiers_map[n.uuid] THEN $branch_from_time
671
+ ELSE $from_time
672
+ END AS row_from_time
673
+ WITH diff_rel_path, r_root, n, r_node, p, diff_rel, has_more_data, row_from_time
674
+ ORDER BY
675
+ %(id_func)s(n) DESC,
676
+ %(id_func)s(p) DESC,
677
+ type(diff_rel),
678
+ r_node.branch = diff_rel.branch DESC,
679
+ r_root.branch = diff_rel.branch DESC,
680
+ diff_rel.from DESC,
681
+ r_node.from DESC,
682
+ r_root.from DESC
683
+ WITH n, p, row_from_time, diff_rel, diff_rel_path, has_more_data
684
+ CALL {
685
+ // -------------------------------------
686
+ // Exclude properties under nodes and attributes/relationships deleted
687
+ // on this branch in the timeframe because those were all handled above
688
+ // -------------------------------------
689
+ WITH n, p, row_from_time
690
+ CALL {
691
+ WITH n, row_from_time
692
+ OPTIONAL MATCH (root:Root)<-[r_root_deleted:IS_PART_OF {branch: $branch_name}]-(n)
693
+ WHERE row_from_time <= r_root_deleted.from < $to_time
694
+ WITH r_root_deleted
695
+ ORDER BY r_root_deleted.status DESC
696
+ LIMIT 1
697
+ RETURN COALESCE(r_root_deleted.status = "deleted", FALSE) AS node_deleted
698
+ }
699
+ WITH n, p, row_from_time, node_deleted
700
+ CALL {
701
+ WITH n, p, row_from_time
702
+ OPTIONAL MATCH (n)-[r_node_deleted {branch: $branch_name}]-(p)
703
+ WHERE row_from_time <= r_node_deleted.from < $to_time
704
+ AND type(r_node_deleted) IN ["HAS_ATTRIBUTE", "IS_RELATED"]
705
+ WITH r_node_deleted
706
+ ORDER BY r_node_deleted.status DESC
707
+ LIMIT 1
708
+ RETURN COALESCE(r_node_deleted.status = "deleted", FALSE) AS field_deleted
709
+ }
710
+ RETURN node_deleted OR field_deleted AS node_or_field_deleted
711
+ }
712
+ WITH n, p, diff_rel, diff_rel_path, has_more_data, node_or_field_deleted
713
+ WHERE node_or_field_deleted = FALSE
714
+ WITH n, p, type(diff_rel) AS drt, head(collect(diff_rel_path)) AS diff_path, has_more_data
715
+ """ % {"id_func": db.get_id_function_name()}
716
+ self.add_to_query(properties_path_query)
717
+ self.add_to_query(self.get_previous_base_path_query(db=db))
718
+ self.add_to_query(self.get_relationship_peer_side_query(db=db))
719
+ self.add_to_query("UNWIND diff_rel_paths AS diff_path")
720
+ self.return_labels = ["DISTINCT diff_path AS diff_path", "has_more_data"]
@@ -479,11 +479,17 @@ class NodeListGetAttributeQuery(Query):
479
479
  self.return_labels = ["n", "a", "av", "r1", "r2"]
480
480
 
481
481
  # Add Is_Protected and Is_visible
482
+ rel_isv_branch_filter, _ = self.branch.get_query_filter_path(
483
+ at=self.at, branch_agnostic=self.branch_agnostic, variable_name="rel_isv"
484
+ )
485
+ rel_isp_branch_filter, _ = self.branch.get_query_filter_path(
486
+ at=self.at, branch_agnostic=self.branch_agnostic, variable_name="rel_isp"
487
+ )
482
488
  query = """
483
489
  MATCH (a)-[rel_isv:IS_VISIBLE]-(isv:Boolean)
484
490
  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}
491
+ WHERE (%(rel_isv_branch_filter)s) AND (%(rel_isp_branch_filter)s)
492
+ """ % {"rel_isv_branch_filter": rel_isv_branch_filter, "rel_isp_branch_filter": rel_isp_branch_filter}
487
493
  self.add_to_query(query)
488
494
 
489
495
  self.return_labels.extend(["isv", "isp", "rel_isv", "rel_isp"])