infrahub-server 1.2.9rc0__py3-none-any.whl → 1.2.11__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 (79) hide show
  1. infrahub/computed_attribute/models.py +13 -0
  2. infrahub/computed_attribute/tasks.py +48 -26
  3. infrahub/config.py +9 -0
  4. infrahub/core/attribute.py +43 -2
  5. infrahub/core/branch/models.py +8 -9
  6. infrahub/core/branch/tasks.py +0 -2
  7. infrahub/core/constants/infrahubkind.py +1 -0
  8. infrahub/core/constraint/node/runner.py +1 -1
  9. infrahub/core/diff/calculator.py +65 -11
  10. infrahub/core/diff/combiner.py +38 -31
  11. infrahub/core/diff/coordinator.py +44 -28
  12. infrahub/core/diff/data_check_synchronizer.py +3 -2
  13. infrahub/core/diff/enricher/hierarchy.py +36 -27
  14. infrahub/core/diff/ipam_diff_parser.py +5 -4
  15. infrahub/core/diff/merger/merger.py +46 -16
  16. infrahub/core/diff/merger/serializer.py +1 -0
  17. infrahub/core/diff/model/field_specifiers_map.py +64 -0
  18. infrahub/core/diff/model/path.py +58 -58
  19. infrahub/core/diff/parent_node_adder.py +14 -16
  20. infrahub/core/diff/query/drop_nodes.py +42 -0
  21. infrahub/core/diff/query/field_specifiers.py +8 -7
  22. infrahub/core/diff/query/filters.py +15 -1
  23. infrahub/core/diff/query/merge.py +264 -28
  24. infrahub/core/diff/query/save.py +6 -2
  25. infrahub/core/diff/query_parser.py +55 -65
  26. infrahub/core/diff/repository/deserializer.py +38 -24
  27. infrahub/core/diff/repository/repository.py +31 -12
  28. infrahub/core/diff/tasks.py +3 -3
  29. infrahub/core/graph/__init__.py +1 -1
  30. infrahub/core/migrations/graph/__init__.py +2 -0
  31. infrahub/core/migrations/graph/m027_delete_isolated_nodes.py +50 -0
  32. infrahub/core/migrations/graph/m028_delete_diffs.py +38 -0
  33. infrahub/core/node/resource_manager/ip_address_pool.py +6 -2
  34. infrahub/core/node/resource_manager/ip_prefix_pool.py +6 -2
  35. infrahub/core/protocols.py +4 -0
  36. infrahub/core/query/branch.py +27 -17
  37. infrahub/core/query/diff.py +169 -51
  38. infrahub/core/query/node.py +39 -5
  39. infrahub/core/query/relationship.py +105 -30
  40. infrahub/core/query/subquery.py +2 -2
  41. infrahub/core/relationship/model.py +1 -1
  42. infrahub/core/schema/definitions/core/__init__.py +8 -1
  43. infrahub/core/schema/definitions/core/resource_pool.py +20 -0
  44. infrahub/core/schema/schema_branch.py +3 -0
  45. infrahub/core/validators/tasks.py +1 -1
  46. infrahub/core/validators/uniqueness/query.py +7 -0
  47. infrahub/database/__init__.py +5 -4
  48. infrahub/graphql/app.py +1 -1
  49. infrahub/graphql/loaders/node.py +1 -1
  50. infrahub/graphql/loaders/peers.py +1 -1
  51. infrahub/graphql/mutations/proposed_change.py +1 -1
  52. infrahub/graphql/queries/diff/tree.py +2 -1
  53. infrahub/graphql/queries/relationship.py +1 -1
  54. infrahub/graphql/queries/task.py +10 -0
  55. infrahub/graphql/resolvers/many_relationship.py +4 -4
  56. infrahub/graphql/resolvers/resolver.py +4 -4
  57. infrahub/graphql/resolvers/single_relationship.py +2 -2
  58. infrahub/graphql/subscription/graphql_query.py +2 -2
  59. infrahub/graphql/types/branch.py +1 -1
  60. infrahub/graphql/types/task_log.py +3 -2
  61. infrahub/message_bus/operations/refresh/registry.py +1 -1
  62. infrahub/task_manager/task.py +44 -4
  63. infrahub/telemetry/database.py +1 -1
  64. infrahub/telemetry/tasks.py +1 -1
  65. infrahub/trigger/models.py +11 -1
  66. infrahub/trigger/setup.py +51 -15
  67. infrahub/trigger/tasks.py +1 -4
  68. infrahub/types.py +1 -1
  69. infrahub/webhook/models.py +2 -1
  70. infrahub/workflows/catalogue.py +9 -0
  71. infrahub/workflows/initialization.py +1 -3
  72. infrahub_sdk/timestamp.py +2 -2
  73. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.2.11.dist-info}/METADATA +3 -3
  74. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.2.11.dist-info}/RECORD +79 -75
  75. infrahub_testcontainers/docker-compose.test.yml +3 -3
  76. infrahub_testcontainers/performance_test.py +6 -3
  77. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.2.11.dist-info}/LICENSE.txt +0 -0
  78. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.2.11.dist-info}/WHEEL +0 -0
  79. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.2.11.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from infrahub.core import registry
6
+ from infrahub.core.diff.repository.repository import DiffRepository
7
+ from infrahub.core.migrations.shared import MigrationResult
8
+ from infrahub.dependencies.registry import build_component_registry, get_component_registry
9
+ from infrahub.log import get_logger
10
+
11
+ from ..shared import ArbitraryMigration
12
+
13
+ if TYPE_CHECKING:
14
+ from infrahub.database import InfrahubDatabase
15
+
16
+ log = get_logger()
17
+
18
+
19
+ class Migration028(ArbitraryMigration):
20
+ """Delete all diffs because of an update to how we store diff information. All diffs will need to be recalculated"""
21
+
22
+ name: str = "028_diff_delete_bug_fix_update"
23
+ minimum_version: int = 27
24
+
25
+ async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
26
+ result = MigrationResult()
27
+
28
+ return result
29
+
30
+ async def execute(self, db: InfrahubDatabase) -> MigrationResult:
31
+ default_branch = registry.get_branch_from_registry()
32
+ build_component_registry()
33
+ component_registry = get_component_registry()
34
+ diff_repo = await component_registry.get_component(DiffRepository, db=db, branch=default_branch)
35
+
36
+ diff_roots = await diff_repo.get_roots_metadata()
37
+ await diff_repo.delete_diff_roots(diff_root_uuids=[d.uuid for d in diff_roots])
38
+ return MigrationResult()
@@ -81,11 +81,15 @@ class CoreIPAddressPool(Node):
81
81
  return node
82
82
 
83
83
  async def get_next(self, db: InfrahubDatabase, prefixlen: int | None = None) -> IPAddressType:
84
- # Measure utilization of all prefixes identified as resources
85
84
  resources = await self.resources.get_peers(db=db) # type: ignore[attr-defined]
86
85
  ip_namespace = await self.ip_namespace.get_peer(db=db) # type: ignore[attr-defined]
87
86
 
88
- for resource in resources.values():
87
+ try:
88
+ weighted_resources = sorted(resources.values(), key=lambda r: r.allocation_weight.value or 0, reverse=True)
89
+ except AttributeError:
90
+ weighted_resources = list(resources.values())
91
+
92
+ for resource in weighted_resources:
89
93
  ip_prefix = ipaddress.ip_network(resource.prefix.value) # type: ignore[attr-defined]
90
94
  prefix_length = prefixlen or ip_prefix.prefixlen
91
95
 
@@ -88,11 +88,15 @@ class CoreIPPrefixPool(Node):
88
88
  return node
89
89
 
90
90
  async def get_next(self, db: InfrahubDatabase, prefixlen: int) -> IPNetworkType:
91
- # Measure utilization of all prefixes identified as resources
92
91
  resources = await self.resources.get_peers(db=db) # type: ignore[attr-defined]
93
92
  ip_namespace = await self.ip_namespace.get_peer(db=db) # type: ignore[attr-defined]
94
93
 
95
- for resource in resources.values():
94
+ try:
95
+ weighted_resources = sorted(resources.values(), key=lambda r: r.allocation_weight.value or 0, reverse=True)
96
+ except AttributeError:
97
+ weighted_resources = list(resources.values())
98
+
99
+ for resource in weighted_resources:
96
100
  subnets = await get_subnets(
97
101
  db=db,
98
102
  ip_prefix=ipaddress.ip_network(resource.prefix.value), # type: ignore[attr-defined]
@@ -209,6 +209,10 @@ class CoreWebhook(CoreNode):
209
209
  validate_certificates: BooleanOptional
210
210
 
211
211
 
212
+ class CoreWeightedPoolResource(CoreNode):
213
+ allocation_weight: IntegerOptional
214
+
215
+
212
216
  class LineageOwner(CoreNode):
213
217
  pass
214
218
 
@@ -51,23 +51,33 @@ class DeleteBranchRelationshipsQuery(Query):
51
51
  super().__init__(**kwargs)
52
52
 
53
53
  async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
54
- if config.SETTINGS.database.db_type == config.DatabaseType.MEMGRAPH:
55
- query = """
56
- MATCH p = (s)-[r]-(d)
57
- WHERE r.branch = $branch_name
58
- DELETE r
59
- """
60
- else:
61
- query = """
62
- MATCH p = (s)-[r]-(d)
63
- WHERE r.branch = $branch_name
64
- DELETE r
65
- WITH *
66
- UNWIND nodes(p) AS n
67
- MATCH (n)
68
- WHERE NOT exists((n)--())
69
- DELETE n
70
- """
54
+ query = """
55
+ MATCH (s)-[r1]-(d)
56
+ WHERE r1.branch = $branch_name
57
+ DELETE r1
58
+
59
+ WITH collect(DISTINCT s) + collect(DISTINCT d) AS nodes
60
+
61
+ // Collect node IDs for filtering
62
+ WITH nodes, [n in nodes | n.uuid] as nodes_uuids
63
+
64
+ // Also delete agnostic relationships that would not have been deleted above
65
+ MATCH (s2: Node)-[r2]-(d2)
66
+ WHERE NOT exists((s2)-[:IS_PART_OF]-(:Root))
67
+ AND s2.uuid IN nodes_uuids
68
+ DELETE r2
69
+
70
+ WITH nodes, collect(DISTINCT s2) + collect(DISTINCT d2) as additional_nodes
71
+
72
+ WITH nodes + additional_nodes as nodes
73
+
74
+ // Delete nodes that are no longer connected to any other nodes
75
+ UNWIND nodes AS n
76
+ WITH DISTINCT n
77
+ MATCH (n)
78
+ WHERE NOT exists((n)--())
79
+ DELETE n
80
+ """
71
81
  self.params["branch_name"] = self.branch_name
72
82
  self.add_to_query(query)
73
83
 
@@ -1,14 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Any
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Any, Generator
4
5
 
5
6
  from infrahub import config
6
- from infrahub.core.constants import GLOBAL_BRANCH_NAME, BranchSupportType
7
+ from infrahub.core.constants import GLOBAL_BRANCH_NAME, BranchSupportType, DiffAction, RelationshipStatus
7
8
  from infrahub.core.query import Query, QueryType
8
9
  from infrahub.core.timestamp import Timestamp
9
10
 
10
11
  if TYPE_CHECKING:
11
12
  from infrahub.core.branch import Branch
13
+ from infrahub.core.diff.model.field_specifiers_map import NodeFieldSpecifierMap
12
14
  from infrahub.database import InfrahubDatabase
13
15
 
14
16
 
@@ -106,8 +108,8 @@ class DiffCalculationQuery(DiffQuery):
106
108
  self,
107
109
  base_branch: Branch,
108
110
  diff_branch_from_time: Timestamp,
109
- current_node_field_specifiers: dict[str, set[str]] | None = None,
110
- new_node_field_specifiers: dict[str, set[str]] | None = None,
111
+ current_node_field_specifiers: NodeFieldSpecifierMap | None = None,
112
+ new_node_field_specifiers: NodeFieldSpecifierMap | None = None,
111
113
  **kwargs: Any,
112
114
  ):
113
115
  self.base_branch = base_branch
@@ -127,12 +129,13 @@ CALL {
127
129
  // add base branch paths before branched_from, if they exist
128
130
  // -------------------------------------
129
131
  WITH n, attr_rel, r_node, r_prop
132
+ // 'base_n' instead of 'n' here to get previous value for node with a migrated kind/inheritance
130
133
  OPTIONAL MATCH latest_base_path = (:Root)<-[base_r_root:IS_PART_OF {branch: $base_branch_name}]
131
- -(n)-[base_r_node {branch: $base_branch_name}]
134
+ -(base_n {uuid: n.uuid})-[base_r_node {branch: $base_branch_name}]
132
135
  -(attr_rel)-[base_r_prop {branch: $base_branch_name}]->(base_prop)
133
136
  WHERE type(base_r_node) = type(r_node)
134
137
  AND type(base_r_prop) = type(r_prop)
135
- AND [%(id_func)s(n), type(base_r_node)] <> [%(id_func)s(base_prop), type(base_r_prop)]
138
+ AND [%(id_func)s(base_n), type(base_r_node)] <> [%(id_func)s(base_prop), type(base_r_prop)]
136
139
  AND all(
137
140
  r in relationships(latest_base_path)
138
141
  WHERE r.from < $branch_from_time
@@ -142,7 +145,7 @@ CALL {
142
145
  // the migration leaves two nodes with the same UUID linked to the same Relationship
143
146
  // ------------------------
144
147
  AND (
145
- n.uuid IS NULL OR base_prop.uuid IS NULL OR n.uuid <> base_prop.uuid
148
+ base_n.uuid IS NULL OR base_prop.uuid IS NULL OR base_n.uuid <> base_prop.uuid
146
149
  OR type(base_r_node) <> "IS_RELATED" OR type(base_r_prop) <> "IS_RELATED"
147
150
  )
148
151
  WITH latest_base_path, base_r_root, base_r_node, base_r_prop
@@ -198,6 +201,13 @@ WITH reduce(
198
201
  diff_rel_paths = [], item IN [penultimate_path, peer_path] |
199
202
  CASE WHEN item IS NULL THEN diff_rel_paths ELSE diff_rel_paths + [item] END
200
203
  ) AS diff_rel_paths, has_more_data
204
+ // ------------------------
205
+ // make sure we still include has_more_data if diff_rel_paths is empty
206
+ // ------------------------
207
+ WITH CASE
208
+ WHEN diff_rel_paths = [] THEN [NULL]
209
+ ELSE diff_rel_paths
210
+ END AS diff_rel_paths, has_more_data
201
211
  """
202
212
 
203
213
  def get_previous_base_path_query(self, db: InfrahubDatabase) -> str:
@@ -231,10 +241,10 @@ class DiffNodePathsQuery(DiffCalculationQuery):
231
241
  self.params.update(params_dict)
232
242
  self.params.update(
233
243
  {
234
- "new_node_ids_list": list(self.new_node_field_specifiers.keys())
244
+ "new_node_ids_list": self.new_node_field_specifiers.get_uuids_list()
235
245
  if self.new_node_field_specifiers
236
246
  else None,
237
- "current_node_ids_list": list(self.current_node_field_specifiers.keys())
247
+ "current_node_ids_list": self.current_node_field_specifiers.get_uuids_list()
238
248
  if self.current_node_field_specifiers
239
249
  else None,
240
250
  }
@@ -276,7 +286,7 @@ WITH p, q, diff_rel, CASE
276
286
  WHEN $new_node_ids_list IS NOT NULL AND p.uuid IN $new_node_ids_list THEN $branch_from_time
277
287
  ELSE $from_time
278
288
  END AS row_from_time
279
- ORDER BY p.uuid DESC
289
+ ORDER BY %(id_func)s(p) DESC
280
290
  SKIP $offset
281
291
  LIMIT $limit
282
292
  // -------------------------------------
@@ -313,15 +323,15 @@ CALL {
313
323
  AND node.branch_support IN [$branch_aware, $branch_agnostic]
314
324
  AND type(r_prop) IN ["IS_VISIBLE", "IS_PROTECTED", "HAS_SOURCE", "HAS_OWNER", "HAS_VALUE", "IS_RELATED"]
315
325
  AND any(l in labels(prop) WHERE l in ["Boolean", "Node", "AttributeValue"])
316
- AND ALL(
317
- r in [r_node, r_prop]
318
- WHERE r.from < $to_time AND r.branch = top_diff_rel.branch
319
- )
320
326
  AND (top_diff_rel.to IS NULL OR top_diff_rel.to >= r_node.from)
321
327
  AND (r_node.to IS NULL OR r_node.to >= r_prop.from)
322
328
  AND [%(id_func)s(p), type(r_node)] <> [%(id_func)s(prop), type(r_prop)]
323
- AND top_diff_rel.status = r_node.status
324
- AND top_diff_rel.status = r_prop.status
329
+ AND r_node.from < $to_time
330
+ AND r_node.branch = top_diff_rel.branch
331
+ AND r_node.status = top_diff_rel.status
332
+ AND r_prop.from < $to_time
333
+ AND r_prop.branch = top_diff_rel.branch
334
+ AND r_prop.status = top_diff_rel.status
325
335
  // ------------------------
326
336
  // special handling for nodes that had their kind updated,
327
337
  // the migration leaves two nodes with the same UUID linked to the same Relationship
@@ -371,15 +381,16 @@ class DiffFieldPathsQuery(DiffCalculationQuery):
371
381
 
372
382
  self.params.update(
373
383
  {
374
- "current_node_field_specifiers_map": {
375
- node_uuid: list(field_names)
376
- for node_uuid, field_names in self.current_node_field_specifiers.items()
377
- }
384
+ "current_node_ids_list": self.current_node_field_specifiers.get_uuids_list()
385
+ if self.current_node_field_specifiers
386
+ else None,
387
+ "new_node_ids_list": self.new_node_field_specifiers.get_uuids_list()
388
+ if self.new_node_field_specifiers
389
+ else None,
390
+ "current_node_field_specifiers_map": self.current_node_field_specifiers.get_uuid_field_names_map()
378
391
  if self.current_node_field_specifiers is not None
379
392
  else None,
380
- "new_node_field_specifiers_map": {
381
- node_uuid: list(field_names) for node_uuid, field_names in self.new_node_field_specifiers.items()
382
- }
393
+ "new_node_field_specifiers_map": self.new_node_field_specifiers.get_uuid_field_names_map()
383
394
  if self.new_node_field_specifiers is not None
384
395
  else None,
385
396
  }
@@ -400,16 +411,16 @@ AND (r_root.to IS NULL OR diff_rel.branch <> r_root.branch OR r_root.to >= diff_
400
411
  // node ID and field name filtering first pass
401
412
  AND (
402
413
  (
403
- $current_node_field_specifiers_map IS NOT NULL
404
- AND $current_node_field_specifiers_map[p.uuid] IS NOT NULL
414
+ $current_node_ids_list IS NOT NULL
415
+ AND p.uuid IN $current_node_ids_list
405
416
  AND q.name IN $current_node_field_specifiers_map[p.uuid]
406
417
  ) OR (
407
- $new_node_field_specifiers_map IS NOT NULL
408
- AND $new_node_field_specifiers_map[p.uuid] IS NOT NULL
418
+ $new_node_ids_list IS NOT NULL
419
+ AND p.uuid IN $new_node_ids_list
409
420
  AND q.name IN $new_node_field_specifiers_map[p.uuid]
410
421
  ) OR (
411
- $current_node_field_specifiers_map IS NULL
412
- AND $new_node_field_specifiers_map IS NULL
422
+ $new_node_ids_list IS NULL
423
+ AND $current_node_ids_list IS NULL
413
424
  )
414
425
  )
415
426
  // node ID and field name filtering second pass
@@ -417,8 +428,12 @@ AND (
417
428
  // time-based filters for nodes already included in the diff or fresh changes
418
429
  (
419
430
  (
420
- ($current_node_field_specifiers_map IS NOT NULL AND q.name IN $current_node_field_specifiers_map[p.uuid])
421
- OR ($current_node_field_specifiers_map IS NULL AND $new_node_field_specifiers_map IS NULL)
431
+ (
432
+ $current_node_ids_list IS NOT NULL
433
+ AND p.uuid IN $current_node_ids_list
434
+ AND q.name IN $current_node_field_specifiers_map[p.uuid]
435
+ )
436
+ OR ($current_node_ids_list IS NULL AND $new_node_ids_list IS NULL)
422
437
  )
423
438
  AND (r_root.from < $from_time OR p.branch_support = $branch_agnostic)
424
439
  AND (
@@ -428,7 +443,11 @@ AND (
428
443
  )
429
444
  // time-based filters for new nodes
430
445
  OR (
431
- ($new_node_field_specifiers_map IS NOT NULL AND q.name IN $new_node_field_specifiers_map[p.uuid])
446
+ (
447
+ $new_node_ids_list IS NOT NULL
448
+ AND p.uuid IN $new_node_ids_list
449
+ AND q.name IN $new_node_field_specifiers_map[p.uuid]
450
+ )
432
451
  AND (r_root.from < $branch_from_time OR p.branch_support = $branch_agnostic)
433
452
  AND (
434
453
  ($branch_from_time <= diff_rel.from < $to_time AND (diff_rel.to IS NULL OR diff_rel.to > $to_time))
@@ -454,7 +473,11 @@ WITH one_result[0] AS root, one_result[1] AS r_root, one_result[2] AS p, one_res
454
473
  // Add correct from_time for row
455
474
  // -------------------------------------
456
475
  WITH root, r_root, p, diff_rel, q, has_more_data, CASE
457
- WHEN $new_node_field_specifiers_map IS NOT NULL AND q.name IN $new_node_field_specifiers_map[p.uuid] THEN $branch_from_time
476
+ WHEN
477
+ $new_node_ids_list IS NOT NULL
478
+ AND p.uuid IN $new_node_ids_list
479
+ AND q.name IN $new_node_field_specifiers_map[p.uuid]
480
+ THEN $branch_from_time
458
481
  ELSE $from_time
459
482
  END AS row_from_time
460
483
  // -------------------------------------
@@ -554,15 +577,16 @@ class DiffPropertyPathsQuery(DiffCalculationQuery):
554
577
 
555
578
  self.params.update(
556
579
  {
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
- }
580
+ "current_node_ids_list": self.current_node_field_specifiers.get_uuids_list()
581
+ if self.current_node_field_specifiers
582
+ else None,
583
+ "new_node_ids_list": self.new_node_field_specifiers.get_uuids_list()
584
+ if self.new_node_field_specifiers
585
+ else None,
586
+ "current_node_field_specifiers_map": self.current_node_field_specifiers.get_uuid_field_names_map()
561
587
  if self.current_node_field_specifiers is not None
562
588
  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
- }
589
+ "new_node_field_specifiers_map": self.new_node_field_specifiers.get_uuid_field_names_map()
566
590
  if self.new_node_field_specifiers is not None
567
591
  else None,
568
592
  }
@@ -580,16 +604,16 @@ AND type(r_node) IN ["HAS_ATTRIBUTE", "IS_RELATED"]
580
604
  // node ID and field name filtering first pass
581
605
  AND (
582
606
  (
583
- $current_node_field_specifiers_map IS NOT NULL
584
- AND $current_node_field_specifiers_map[n.uuid] IS NOT NULL
607
+ $current_node_ids_list IS NOT NULL
608
+ AND n.uuid IN $current_node_ids_list
585
609
  AND p.name IN $current_node_field_specifiers_map[n.uuid]
586
610
  ) OR (
587
- $new_node_field_specifiers_map IS NOT NULL
588
- AND $new_node_field_specifiers_map[n.uuid] IS NOT NULL
611
+ $new_node_ids_list IS NOT NULL
612
+ AND n.uuid IN $new_node_ids_list
589
613
  AND p.name IN $new_node_field_specifiers_map[n.uuid]
590
614
  ) OR (
591
- $current_node_field_specifiers_map IS NULL
592
- AND $new_node_field_specifiers_map IS NULL
615
+ $new_node_ids_list IS NULL
616
+ AND $current_node_ids_list IS NULL
593
617
  )
594
618
  )
595
619
  // node ID and field name filtering second pass
@@ -597,8 +621,12 @@ AND (
597
621
  // time-based filters for nodes already included in the diff or fresh changes
598
622
  (
599
623
  (
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)
624
+ (
625
+ $current_node_ids_list IS NOT NULL
626
+ AND n.uuid IN $current_node_ids_list
627
+ AND p.name IN $current_node_field_specifiers_map[n.uuid]
628
+ )
629
+ OR ($current_node_ids_list IS NULL AND $new_node_ids_list IS NULL)
602
630
  )
603
631
  AND (
604
632
  ($from_time <= diff_rel.from < $to_time AND (diff_rel.to IS NULL OR diff_rel.to > $to_time))
@@ -612,7 +640,11 @@ AND (
612
640
  )
613
641
  // time-based filters for new nodes
614
642
  OR (
615
- ($new_node_field_specifiers_map IS NOT NULL AND p.name IN $new_node_field_specifiers_map[n.uuid])
643
+ (
644
+ $new_node_ids_list IS NOT NULL
645
+ AND n.uuid IN $new_node_ids_list
646
+ AND p.name IN $new_node_field_specifiers_map[n.uuid]
647
+ )
616
648
  AND (
617
649
  ($branch_from_time <= diff_rel.from < $to_time AND (diff_rel.to IS NULL OR diff_rel.to > $to_time))
618
650
  OR ($branch_from_time <= diff_rel.to < $to_time)
@@ -667,7 +699,11 @@ WITH one_result[0] AS diff_rel_path, one_result[1] AS r_root, one_result[2] AS n
667
699
  // Add correct from_time for row
668
700
  // -------------------------------------
669
701
  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
702
+ WHEN
703
+ $new_node_ids_list IS NOT NULL
704
+ AND n.uuid IN $new_node_ids_list
705
+ AND p.name IN $new_node_field_specifiers_map[n.uuid]
706
+ THEN $branch_from_time
671
707
  ELSE $from_time
672
708
  END AS row_from_time
673
709
  WITH diff_rel_path, r_root, n, r_node, p, diff_rel, has_more_data, row_from_time
@@ -690,7 +726,7 @@ CALL {
690
726
  CALL {
691
727
  WITH n, row_from_time
692
728
  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
729
+ WHERE r_root_deleted.from < $to_time
694
730
  WITH r_root_deleted
695
731
  ORDER BY r_root_deleted.status DESC
696
732
  LIMIT 1
@@ -718,3 +754,85 @@ WITH n, p, type(diff_rel) AS drt, head(collect(diff_rel_path)) AS diff_path, has
718
754
  self.add_to_query(self.get_relationship_peer_side_query(db=db))
719
755
  self.add_to_query("UNWIND diff_rel_paths AS diff_path")
720
756
  self.return_labels = ["DISTINCT diff_path AS diff_path", "has_more_data"]
757
+
758
+
759
+ @dataclass
760
+ class MigratedKindNode:
761
+ uuid: str
762
+ kind: str
763
+ db_id: str
764
+ from_time: Timestamp
765
+ action: DiffAction
766
+ has_more_data: bool
767
+
768
+
769
+ class DiffMigratedKindNodesQuery(DiffCalculationQuery):
770
+ name = "diff_migrated_kind_nodes_query"
771
+
772
+ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
773
+ params_dict = self.get_params()
774
+ self.params.update(params_dict)
775
+ migrated_kind_nodes_query = """
776
+ // -------------------------------------
777
+ // Identify nodes added/removed on branch in the time frame
778
+ // -------------------------------------
779
+ MATCH (:Root)<-[diff_rel:IS_PART_OF {branch: $branch_name}]-(n:Node)
780
+ WHERE (
781
+ ($from_time <= diff_rel.from < $to_time AND (diff_rel.to IS NULL OR diff_rel.to > $to_time))
782
+ OR ($from_time <= diff_rel.to < $to_time)
783
+ )
784
+ AND n.branch_support = $branch_aware
785
+ WITH DISTINCT n.uuid AS node_uuid, %(id_func)s(n) AS db_id
786
+ WITH node_uuid, count(*) AS num_nodes_with_uuid
787
+ WHERE num_nodes_with_uuid > 1
788
+ // -------------------------------------
789
+ // Limit the number of nodes
790
+ // -------------------------------------
791
+ WITH node_uuid
792
+ ORDER BY node_uuid
793
+ SKIP $offset
794
+ LIMIT $limit
795
+ WITH collect(node_uuid) AS node_uuids
796
+ WITH node_uuids, size(node_uuids) = $limit AS has_more_data
797
+ MATCH (:Root)<-[diff_rel:IS_PART_OF {branch: $branch_name}]-(n:Node)
798
+ WHERE n.uuid IN node_uuids
799
+ AND (
800
+ ($from_time <= diff_rel.from < $to_time AND (diff_rel.to IS NULL OR diff_rel.to > $to_time))
801
+ OR ($from_time <= diff_rel.to < $to_time)
802
+ )
803
+ // -------------------------------------
804
+ // Ignore node created and deleted on this branch
805
+ // -------------------------------------
806
+ CALL {
807
+ WITH n
808
+ OPTIONAL MATCH (:Root)<-[diff_rel:IS_PART_OF {branch: $branch_name}]-(n)
809
+ WITH diff_rel
810
+ ORDER BY diff_rel.from ASC
811
+ WITH collect(diff_rel.status) AS statuses
812
+ RETURN statuses = ["active", "deleted"] AS intra_branch_update
813
+ }
814
+ WITH n.uuid AS uuid, n.kind AS kind, %(id_func)s(n) AS db_id, diff_rel.from_time AS from_time, diff_rel.status AS status, has_more_data
815
+ WHERE intra_branch_update = FALSE
816
+ """ % {"id_func": db.get_id_function_name()}
817
+ self.add_to_query(query=migrated_kind_nodes_query)
818
+ self.return_labels = [
819
+ "uuid",
820
+ "kind",
821
+ "db_id",
822
+ "from_time",
823
+ "status",
824
+ "has_more_data",
825
+ ]
826
+
827
+ def get_migrated_kind_nodes(self) -> Generator[MigratedKindNode, None, None]:
828
+ for result in self.get_results():
829
+ yield MigratedKindNode(
830
+ uuid=result.get_as_type("uuid", return_type=str),
831
+ kind=result.get_as_type("kind", return_type=str),
832
+ db_id=result.get_as_type("db_id", return_type=str),
833
+ from_time=result.get_as_type("from_time", return_type=Timestamp),
834
+ action=DiffAction.REMOVED
835
+ if result.get_as_type("status", return_type=str).lower() == RelationshipStatus.DELETED.value
836
+ else DiffAction.ADDED,
837
+ has_more_data=result.get_as_type("has_more_data", bool),
838
+ )
@@ -92,6 +92,7 @@ class NodeAttributesFromDB:
92
92
  class PeerInfo:
93
93
  uuid: str
94
94
  kind: str
95
+ db_id: str
95
96
 
96
97
 
97
98
  class NodeQuery(Query):
@@ -412,9 +413,32 @@ class NodeDeleteQuery(NodeQuery):
412
413
  self.params["branch"] = self.branch.name
413
414
  self.params["branch_level"] = self.branch.hierarchy_level
414
415
 
416
+ if self.branch.is_global or self.branch.is_default:
417
+ node_query_match = """
418
+ MATCH (n:Node { uuid: $uuid })
419
+ OPTIONAL MATCH (n)-[delete_edge:IS_PART_OF {status: "deleted", branch: $branch}]->(:Root)
420
+ WHERE delete_edge.from <= $at
421
+ WITH n WHERE delete_edge IS NULL
422
+ """
423
+ else:
424
+ node_filter, node_filter_params = self.branch.get_query_filter_path(at=self.at, variable_name="r")
425
+ node_query_match = """
426
+ MATCH (n:Node { uuid: $uuid })
427
+ CALL {
428
+ WITH n
429
+ MATCH (n)-[r:IS_PART_OF]->(:Root)
430
+ WHERE %(node_filter)s
431
+ RETURN r.status = "active" AS is_active
432
+ ORDER BY r.from DESC
433
+ LIMIT 1
434
+ }
435
+ WITH n WHERE is_active = TRUE
436
+ """ % {"node_filter": node_filter}
437
+ self.params.update(node_filter_params)
438
+ self.add_to_query(node_query_match)
439
+
415
440
  query = """
416
441
  MATCH (root:Root)
417
- MATCH (n:Node { uuid: $uuid })
418
442
  CREATE (n)-[r:IS_PART_OF { branch: $branch, branch_level: $branch_level, status: "deleted", from: $at }]->(root)
419
443
  """
420
444
 
@@ -1405,7 +1429,7 @@ class NodeGetHierarchyQuery(Query):
1405
1429
 
1406
1430
  super().__init__(**kwargs)
1407
1431
 
1408
- async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
1432
+ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002,PLR0915
1409
1433
  hierarchy_schema = self.node_schema.get_hierarchy_schema(db=db, branch=self.branch)
1410
1434
  branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at.to_string())
1411
1435
  self.params.update(branch_params)
@@ -1438,6 +1462,10 @@ class NodeGetHierarchyQuery(Query):
1438
1462
  UNWIND peers_with_duplicates AS pwd
1439
1463
  RETURN DISTINCT pwd AS peer
1440
1464
  }
1465
+ """ % {"filter": filter_str, "branch_filter": branch_filter}
1466
+
1467
+ if not self.branch.is_default:
1468
+ query += """
1441
1469
  CALL {
1442
1470
  WITH n, peer
1443
1471
  MATCH path = (n)%(filter)s(peer)
@@ -1448,10 +1476,14 @@ class NodeGetHierarchyQuery(Query):
1448
1476
  LIMIT 1
1449
1477
  }
1450
1478
  WITH peer1 as peer, is_active
1451
- """ % {"filter": filter_str, "branch_filter": branch_filter, "with_clause": with_clause}
1479
+ """ % {"filter": filter_str, "branch_filter": branch_filter, "with_clause": with_clause}
1480
+ else:
1481
+ query += """
1482
+ WITH peer
1483
+ """
1452
1484
 
1453
1485
  self.add_to_query(query)
1454
- where_clause = ["is_active = TRUE"]
1486
+ where_clause = ["is_active = TRUE"] if not self.branch.is_default else []
1455
1487
 
1456
1488
  clean_filters = extract_field_filters(field_name=self.direction.value, filters=self.filters)
1457
1489
 
@@ -1461,7 +1493,8 @@ class NodeGetHierarchyQuery(Query):
1461
1493
  if clean_filters.get("id", None):
1462
1494
  self.params["peer_ids"].append(clean_filters.get("id"))
1463
1495
 
1464
- self.add_to_query("WHERE " + " AND ".join(where_clause))
1496
+ if where_clause:
1497
+ self.add_to_query("WHERE " + " AND ".join(where_clause))
1465
1498
 
1466
1499
  self.return_labels = ["peer"]
1467
1500
 
@@ -1544,4 +1577,5 @@ class NodeGetHierarchyQuery(Query):
1544
1577
  yield PeerInfo(
1545
1578
  uuid=peer_node.get("uuid"),
1546
1579
  kind=peer_node.get("kind"),
1580
+ db_id=peer_node.element_id,
1547
1581
  )