infrahub-server 1.3.2__py3-none-any.whl → 1.3.4__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 (59) hide show
  1. infrahub/api/schema.py +2 -2
  2. infrahub/cli/db.py +194 -13
  3. infrahub/core/branch/enums.py +8 -0
  4. infrahub/core/branch/models.py +28 -5
  5. infrahub/core/branch/tasks.py +5 -7
  6. infrahub/core/convert_object_type/conversion.py +10 -0
  7. infrahub/core/diff/coordinator.py +32 -34
  8. infrahub/core/diff/diff_locker.py +26 -0
  9. infrahub/core/diff/enricher/hierarchy.py +7 -3
  10. infrahub/core/diff/query_parser.py +7 -3
  11. infrahub/core/graph/__init__.py +1 -1
  12. infrahub/core/initialization.py +4 -3
  13. infrahub/core/merge.py +31 -16
  14. infrahub/core/migrations/graph/__init__.py +26 -0
  15. infrahub/core/migrations/graph/m012_convert_account_generic.py +4 -3
  16. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +4 -3
  17. infrahub/core/migrations/graph/m032_cleanup_orphaned_branch_relationships.py +105 -0
  18. infrahub/core/migrations/graph/m033_deduplicate_relationship_vertices.py +97 -0
  19. infrahub/core/migrations/graph/m034_find_orphaned_schema_fields.py +84 -0
  20. infrahub/core/migrations/schema/node_attribute_add.py +55 -2
  21. infrahub/core/migrations/shared.py +37 -9
  22. infrahub/core/node/__init__.py +44 -21
  23. infrahub/core/node/resource_manager/ip_address_pool.py +5 -3
  24. infrahub/core/node/resource_manager/ip_prefix_pool.py +7 -4
  25. infrahub/core/node/resource_manager/number_pool.py +62 -22
  26. infrahub/core/node/standard.py +4 -0
  27. infrahub/core/query/branch.py +25 -56
  28. infrahub/core/query/node.py +78 -24
  29. infrahub/core/query/relationship.py +11 -8
  30. infrahub/core/query/resource_manager.py +117 -20
  31. infrahub/core/relationship/model.py +10 -5
  32. infrahub/core/schema/__init__.py +5 -0
  33. infrahub/core/schema/attribute_parameters.py +6 -0
  34. infrahub/core/schema/attribute_schema.py +6 -0
  35. infrahub/core/schema/manager.py +5 -11
  36. infrahub/core/schema/relationship_schema.py +6 -0
  37. infrahub/core/schema/schema_branch.py +50 -11
  38. infrahub/core/validators/node/attribute.py +15 -0
  39. infrahub/core/validators/tasks.py +12 -4
  40. infrahub/dependencies/builder/diff/coordinator.py +3 -0
  41. infrahub/dependencies/builder/diff/locker.py +8 -0
  42. infrahub/graphql/mutations/main.py +7 -2
  43. infrahub/graphql/mutations/tasks.py +2 -0
  44. infrahub/graphql/queries/resource_manager.py +4 -4
  45. infrahub/tasks/registry.py +63 -35
  46. infrahub_sdk/client.py +7 -8
  47. infrahub_sdk/ctl/utils.py +3 -0
  48. infrahub_sdk/node/node.py +6 -6
  49. infrahub_sdk/node/relationship.py +43 -2
  50. infrahub_sdk/yaml.py +13 -7
  51. infrahub_server-1.3.4.dist-info/LICENSE.txt +201 -0
  52. {infrahub_server-1.3.2.dist-info → infrahub_server-1.3.4.dist-info}/METADATA +3 -3
  53. {infrahub_server-1.3.2.dist-info → infrahub_server-1.3.4.dist-info}/RECORD +58 -52
  54. infrahub_testcontainers/container.py +1 -1
  55. infrahub_testcontainers/docker-compose-cluster.test.yml +3 -0
  56. infrahub_testcontainers/docker-compose.test.yml +1 -0
  57. infrahub_server-1.3.2.dist-info/LICENSE.txt +0 -661
  58. {infrahub_server-1.3.2.dist-info → infrahub_server-1.3.4.dist-info}/WHEEL +0 -0
  59. {infrahub_server-1.3.2.dist-info → infrahub_server-1.3.4.dist-info}/entry_points.txt +0 -0
@@ -26,6 +26,7 @@ from infrahub.core.protocols import CoreNumberPool, CoreObjectTemplate
26
26
  from infrahub.core.query.node import NodeCheckIDQuery, NodeCreateAllQuery, NodeDeleteQuery, NodeGetListQuery
27
27
  from infrahub.core.schema import (
28
28
  AttributeSchema,
29
+ GenericSchema,
29
30
  NodeSchema,
30
31
  NonGenericSchemaTypes,
31
32
  ProfileSchema,
@@ -82,6 +83,9 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
82
83
  def get_schema(self) -> NonGenericSchemaTypes:
83
84
  return self._schema
84
85
 
86
+ def get_branch(self) -> Branch:
87
+ return self._branch
88
+
85
89
  def get_kind(self) -> str:
86
90
  """Return the main Kind of the Object."""
87
91
  return self._schema.kind
@@ -260,11 +264,9 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
260
264
  within the create code.
261
265
  """
262
266
 
263
- number_pool_parameters: NumberPoolParameters | None = None
264
267
  if attribute.schema.kind == "NumberPool" and isinstance(attribute.schema.parameters, NumberPoolParameters):
265
268
  attribute.from_pool = {"id": attribute.schema.parameters.number_pool_id}
266
269
  attribute.is_default = False
267
- number_pool_parameters = attribute.schema.parameters
268
270
 
269
271
  if not attribute.from_pool:
270
272
  return
@@ -274,9 +276,9 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
274
276
  db=db, id=attribute.from_pool["id"], kind=CoreNumberPool
275
277
  )
276
278
  except NodeNotFoundError:
277
- if number_pool_parameters:
278
- number_pool = await self._fetch_or_create_number_pool(
279
- db=db, attribute=attribute, number_pool_parameters=number_pool_parameters
279
+ if attribute.schema.kind == "NumberPool" and isinstance(attribute.schema.parameters, NumberPoolParameters):
280
+ number_pool = await self.fetch_or_create_number_pool(
281
+ db=db, schema_node=self._schema, schema_attribute=attribute.schema, branch=self._branch
280
282
  )
281
283
 
282
284
  else:
@@ -292,7 +294,9 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
292
294
  and number_pool.node_attribute.value == attribute.name
293
295
  ):
294
296
  try:
295
- next_free = await number_pool.get_resource(db=db, branch=self._branch, node=self, attribute=attribute)
297
+ next_free = await number_pool.get_resource(
298
+ db=db, branch=self._branch, node=self, attribute=attribute.schema
299
+ )
296
300
  except PoolExhaustedError:
297
301
  errors.append(
298
302
  ValidationError({f"{attribute.name}.from_pool": f"The pool {number_pool.node.value} is exhausted."})
@@ -310,10 +314,28 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
310
314
  )
311
315
  )
312
316
 
313
- async def _fetch_or_create_number_pool(
314
- self, db: InfrahubDatabase, attribute: BaseAttribute, number_pool_parameters: NumberPoolParameters
317
+ @staticmethod
318
+ async def fetch_or_create_number_pool(
319
+ db: InfrahubDatabase,
320
+ schema_node: NodeSchema | GenericSchema,
321
+ schema_attribute: AttributeSchema,
322
+ branch: Branch | None = None,
315
323
  ) -> CoreNumberPool:
324
+ """Fetch or create a number pool based on the schema attribute parameters.
325
+
326
+ Warning, ideally this method should be outside of the Node class, but it is itself using the Node class to create the pool node.
327
+ """
328
+
329
+ if (
330
+ schema_attribute.kind != "NumberPool"
331
+ or not schema_attribute.parameters
332
+ or not isinstance(schema_attribute.parameters, NumberPoolParameters)
333
+ ):
334
+ raise ValueError("Attribute is not of type NumberPool")
335
+
316
336
  number_pool_from_db: CoreNumberPool | None = None
337
+ number_pool_parameters: NumberPoolParameters = schema_attribute.parameters
338
+
317
339
  lock_definition = NumberPoolLockDefinition(pool_id=str(number_pool_parameters.number_pool_id))
318
340
  async with lock.registry.get(
319
341
  name=lock_definition.lock_name, namespace=lock_definition.namespace_name, local=False
@@ -322,37 +344,37 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
322
344
  number_pool_from_db = await registry.manager.get_one_by_id_or_default_filter(
323
345
  db=db, id=str(number_pool_parameters.number_pool_id), kind=CoreNumberPool
324
346
  )
347
+ return number_pool_from_db # type: ignore[return-value]
348
+
325
349
  except NodeNotFoundError:
326
350
  schema = db.schema.get_node_schema(name="CoreNumberPool", duplicate=False)
327
351
 
328
- pool_node = self._schema.kind
329
- schema_attribute = self._schema.get_attribute(attribute.schema.name)
352
+ pool_node = schema_node.kind
330
353
  if schema_attribute.inherited:
331
- for generic_name in self._schema.inherit_from:
354
+ for generic_name in schema_node.inherit_from:
332
355
  generic_node = db.schema.get_generic_schema(name=generic_name, duplicate=False)
333
- if attribute.schema.name in generic_node.attribute_names:
356
+ if schema_attribute.name in generic_node.attribute_names:
334
357
  pool_node = generic_node.kind
335
358
  break
336
359
 
337
- number_pool = await Node.init(db=db, schema=schema, branch=self._branch)
360
+ number_pool = await Node.init(db=db, schema=schema, branch=branch)
338
361
  await number_pool.new(
339
362
  db=db,
340
363
  id=number_pool_parameters.number_pool_id,
341
- name=f"{pool_node}.{attribute.schema.name} [{number_pool_parameters.number_pool_id}]",
364
+ name=f"{pool_node}.{schema_attribute.name} [{number_pool_parameters.number_pool_id}]",
342
365
  node=pool_node,
343
- node_attribute=attribute.schema.name,
366
+ node_attribute=schema_attribute.name,
344
367
  start_range=number_pool_parameters.start_range,
345
368
  end_range=number_pool_parameters.end_range,
346
369
  pool_type=NumberPoolType.SCHEMA.value,
347
370
  )
348
371
  await number_pool.save(db=db)
349
372
 
350
- # Do a lookup of the number pool to get the correct mapped type from the registry
351
- # without this we don't get access to the .get_resource() method.
352
- created_pool: CoreNumberPool = number_pool_from_db or await registry.manager.get_one_by_id_or_default_filter(
353
- db=db, id=number_pool.id, kind=CoreNumberPool
354
- )
355
- return created_pool
373
+ # Do a lookup of the number pool to get the correct mapped type from the registry
374
+ # without this we don't get access to the .get_resource() method.
375
+ return await registry.manager.get_one_by_id_or_default_filter(
376
+ db=db, id=number_pool.id, kind=CoreNumberPool
377
+ )
356
378
 
357
379
  async def handle_object_template(self, fields: dict, db: InfrahubDatabase, errors: list) -> None:
358
380
  """Fill the `fields` parameters with values from an object template if one is in use."""
@@ -816,6 +838,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
816
838
 
817
839
  query = await NodeDeleteQuery.init(db=db, node=self, at=delete_at)
818
840
  await query.execute(db=db)
841
+
819
842
  self._node_changelog = node_changelog
820
843
 
821
844
  async def to_graphql(
@@ -18,6 +18,7 @@ from .. import Node
18
18
  if TYPE_CHECKING:
19
19
  from infrahub.core.branch import Branch
20
20
  from infrahub.core.ipam.constants import IPAddressType
21
+ from infrahub.core.timestamp import Timestamp
21
22
  from infrahub.database import InfrahubDatabase
22
23
 
23
24
 
@@ -30,6 +31,7 @@ class CoreIPAddressPool(Node):
30
31
  data: dict[str, Any] | None = None,
31
32
  address_type: str | None = None,
32
33
  prefixlen: int | None = None,
34
+ at: Timestamp | None = None,
33
35
  ) -> Node:
34
36
  # Check if there is already a resource allocated with this identifier
35
37
  # if not, pull all existing prefixes and allocated the next available
@@ -63,18 +65,18 @@ class CoreIPAddressPool(Node):
63
65
  next_address = await self.get_next(db=db, prefixlen=prefixlen)
64
66
 
65
67
  target_schema = registry.get_node_schema(name=address_type, branch=branch)
66
- node = await Node.init(db=db, schema=target_schema, branch=branch)
68
+ node = await Node.init(db=db, schema=target_schema, branch=branch, at=at)
67
69
  try:
68
70
  await node.new(db=db, address=str(next_address), ip_namespace=ip_namespace, **data)
69
71
  except ValidationError as exc:
70
72
  raise ValueError(f"IPAddressPool: {self.name.value} | {exc!s}") from exc # type: ignore[attr-defined]
71
- await node.save(db=db)
73
+ await node.save(db=db, at=at)
72
74
  reconciler = IpamReconciler(db=db, branch=branch)
73
75
  await reconciler.reconcile(ip_value=next_address, namespace=ip_namespace.id, node_uuid=node.get_id())
74
76
 
75
77
  if identifier:
76
78
  query_set = await IPAddressPoolSetReserved.init(
77
- db=db, pool_id=self.id, identifier=identifier, address_id=node.id
79
+ db=db, pool_id=self.id, identifier=identifier, address_id=node.id, at=at
78
80
  )
79
81
  await query_set.execute(db=db)
80
82
 
@@ -20,6 +20,7 @@ from .. import Node
20
20
  if TYPE_CHECKING:
21
21
  from infrahub.core.branch import Branch
22
22
  from infrahub.core.ipam.constants import IPNetworkType
23
+ from infrahub.core.timestamp import Timestamp
23
24
  from infrahub.database import InfrahubDatabase
24
25
 
25
26
 
@@ -33,6 +34,7 @@ class CoreIPPrefixPool(Node):
33
34
  prefixlen: int | None = None,
34
35
  member_type: str | None = None,
35
36
  prefix_type: str | None = None,
37
+ at: Timestamp | None = None,
36
38
  ) -> Node:
37
39
  # Check if there is already a resource allocated with this identifier
38
40
  # if not, pull all existing prefixes and allocated the next available
@@ -68,20 +70,21 @@ class CoreIPPrefixPool(Node):
68
70
  )
69
71
 
70
72
  member_type = member_type or data.get("member_type", None) or self.default_member_type.value.value # type: ignore[attr-defined]
73
+ data["member_type"] = member_type
71
74
 
72
75
  target_schema = registry.get_node_schema(name=prefix_type, branch=branch)
73
- node = await Node.init(db=db, schema=target_schema, branch=branch)
76
+ node = await Node.init(db=db, schema=target_schema, branch=branch, at=at)
74
77
  try:
75
- await node.new(db=db, prefix=str(next_prefix), member_type=member_type, ip_namespace=ip_namespace, **data)
78
+ await node.new(db=db, prefix=str(next_prefix), ip_namespace=ip_namespace, **data)
76
79
  except ValidationError as exc:
77
80
  raise ValueError(f"IPPrefixPool: {self.name.value} | {exc!s}") from exc # type: ignore[attr-defined]
78
- await node.save(db=db)
81
+ await node.save(db=db, at=at)
79
82
  reconciler = IpamReconciler(db=db, branch=branch)
80
83
  await reconciler.reconcile(ip_value=next_prefix, namespace=ip_namespace.id, node_uuid=node.get_id())
81
84
 
82
85
  if identifier:
83
86
  query_set = await PrefixPoolSetReserved.init(
84
- db=db, pool_id=self.id, identifier=identifier, prefix_id=node.id
87
+ db=db, pool_id=self.id, identifier=identifier, prefix_id=node.id, at=at
85
88
  )
86
89
  await query_set.execute(db=db)
87
90
 
@@ -10,16 +10,15 @@ from infrahub.exceptions import PoolExhaustedError
10
10
  from .. import Node
11
11
 
12
12
  if TYPE_CHECKING:
13
- from infrahub.core.attribute import BaseAttribute
14
13
  from infrahub.core.branch import Branch
14
+ from infrahub.core.schema import AttributeSchema
15
+ from infrahub.core.timestamp import Timestamp
15
16
  from infrahub.database import InfrahubDatabase
16
17
 
17
18
 
18
19
  class CoreNumberPool(Node):
19
20
  def get_attribute_nb_excluded_values(self) -> int:
20
- """
21
- Returns the number of excluded values for the attribute of the number pool.
22
- """
21
+ """Returns the number of excluded values for the attribute of the number pool."""
23
22
 
24
23
  pool_node = registry.schema.get(name=self.node.value) # type: ignore [attr-defined]
25
24
  attribute = [attribute for attribute in pool_node.attributes if attribute.name == self.node_attribute.value][0] # type: ignore [attr-defined]
@@ -34,17 +33,42 @@ class CoreNumberPool(Node):
34
33
  res = len(attribute.parameters.get_excluded_single_values()) + sum_excluded_values
35
34
  return res
36
35
 
36
+ async def get_used(
37
+ self,
38
+ db: InfrahubDatabase,
39
+ branch: Branch,
40
+ ) -> list[int]:
41
+ """Returns a list of used numbers in the pool."""
42
+
43
+ query = await NumberPoolGetUsed.init(db=db, branch=branch, pool=self, branch_agnostic=True)
44
+ await query.execute(db=db)
45
+ used = [result.value for result in query.iter_results()]
46
+ return [item for item in used if item is not None]
47
+
48
+ async def reserve(self, db: InfrahubDatabase, number: int, identifier: str, at: Timestamp | None = None) -> None:
49
+ """Reserve a number in the pool for a specific identifier."""
50
+
51
+ query = await NumberPoolSetReserved.init(
52
+ db=db, pool_id=self.get_id(), identifier=identifier, reserved=number, at=at
53
+ )
54
+ await query.execute(db=db)
55
+
37
56
  async def get_resource(
38
57
  self,
39
58
  db: InfrahubDatabase,
40
59
  branch: Branch,
41
60
  node: Node,
42
- attribute: BaseAttribute,
61
+ attribute: AttributeSchema,
43
62
  identifier: str | None = None,
63
+ at: Timestamp | None = None,
44
64
  ) -> int:
65
+ # NOTE: ideally we should use the HFID as the identifier (if available)
66
+ # one of the challenge with using the HFID is that it might change over time
67
+ # so we need to ensure that the identifier is stable, or we need to handle the case where the identifier changes
45
68
  identifier = identifier or node.get_id()
69
+
46
70
  # Check if there is already a resource allocated with this identifier
47
- # if not, pull all existing prefixes and allocated the next available
71
+ # if not, pull all existing number and allocate the next available
48
72
  # TODO add support for branch, if the node is reserved with this id in another branch we should return an error
49
73
  query_get = await NumberPoolGetReserved.init(db=db, branch=branch, pool_id=self.id, identifier=identifier)
50
74
  await query_get.execute(db=db)
@@ -54,35 +78,51 @@ class CoreNumberPool(Node):
54
78
 
55
79
  # If we have not returned a value we need to find one if avaiable
56
80
  number = await self.get_next(db=db, branch=branch, attribute=attribute)
57
-
58
- query_set = await NumberPoolSetReserved.init(
59
- db=db, pool_id=self.get_id(), identifier=identifier, reserved=number
60
- )
61
- await query_set.execute(db=db)
81
+ await self.reserve(db=db, number=number, identifier=identifier, at=at)
62
82
  return number
63
83
 
64
- async def get_next(self, db: InfrahubDatabase, branch: Branch, attribute: BaseAttribute) -> int:
65
- query = await NumberPoolGetUsed.init(db=db, branch=branch, pool=self, branch_agnostic=True)
66
- await query.execute(db=db)
67
- taken = [result.get_as_optional_type("av.value", return_type=int) for result in query.results]
68
- parameters = attribute.schema.parameters
84
+ async def get_next(self, db: InfrahubDatabase, branch: Branch, attribute: AttributeSchema) -> int:
85
+ taken = await self.get_used(db=db, branch=branch)
86
+
69
87
  next_number = find_next_free(
70
88
  start=self.start_range.value, # type: ignore[attr-defined]
71
89
  end=self.end_range.value, # type: ignore[attr-defined]
72
90
  taken=taken,
73
- parameters=parameters if isinstance(parameters, NumberAttributeParameters) else None,
91
+ parameters=attribute.parameters if isinstance(attribute.parameters, NumberAttributeParameters) else None,
74
92
  )
75
93
  if next_number is None:
76
94
  raise PoolExhaustedError("There are no more values available in this pool.")
77
95
 
78
96
  return next_number
79
97
 
98
+ async def get_next_many(
99
+ self, db: InfrahubDatabase, quantity: int, branch: Branch, attribute: AttributeSchema
100
+ ) -> list[int]:
101
+ taken = await self.get_used(db=db, branch=branch)
102
+
103
+ allocated: list[int] = []
104
+
105
+ for _ in range(quantity):
106
+ next_number = find_next_free(
107
+ start=self.start_range.value, # type: ignore[attr-defined]
108
+ end=self.end_range.value, # type: ignore[attr-defined]
109
+ taken=list(set(taken) | set(allocated)),
110
+ parameters=attribute.parameters
111
+ if isinstance(attribute.parameters, NumberAttributeParameters)
112
+ else None,
113
+ )
114
+ if next_number is None:
115
+ raise PoolExhaustedError(
116
+ f"There are no more values available in this pool, couldn't allocate {quantity} values, only {len(allocated)} available."
117
+ )
118
+
119
+ allocated.append(next_number)
120
+
121
+ return allocated
122
+
80
123
 
81
- def find_next_free(
82
- start: int, end: int, taken: list[int | None], parameters: NumberAttributeParameters | None
83
- ) -> int | None:
84
- used_numbers = [number for number in taken if number is not None]
85
- used_set = set(used_numbers)
124
+ def find_next_free(start: int, end: int, taken: list[int], parameters: NumberAttributeParameters | None) -> int | None:
125
+ used_set = set(taken)
86
126
 
87
127
  for num in range(start, end + 1):
88
128
  if num not in used_set:
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
+ from enum import Enum
4
5
  from typing import TYPE_CHECKING, Any, Optional, Union, get_args, get_origin
5
6
  from uuid import UUID
6
7
 
@@ -191,6 +192,9 @@ class StandardNode(BaseModel):
191
192
  continue
192
193
 
193
194
  attr_value = getattr(self, attr_name)
195
+ if isinstance(attr_value, Enum):
196
+ attr_value = attr_value.value
197
+
194
198
  field_type = self.guess_field_type(field)
195
199
 
196
200
  if attr_value is None:
@@ -3,43 +3,12 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from infrahub import config
6
- from infrahub.core.constants import RelationshipStatus
7
6
  from infrahub.core.query import Query, QueryType
8
7
 
9
8
  if TYPE_CHECKING:
10
9
  from infrahub.database import InfrahubDatabase
11
10
 
12
11
 
13
- class AddNodeToBranch(Query):
14
- name: str = "node_add_to_branch"
15
- insert_return: bool = False
16
-
17
- type: QueryType = QueryType.WRITE
18
-
19
- def __init__(self, node_id: int, **kwargs: Any):
20
- self.node_id = node_id
21
- super().__init__(**kwargs)
22
-
23
- async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
24
- query = """
25
- MATCH (root:Root)
26
- MATCH (d) WHERE %(id_func)s(d) = $node_id
27
- WITH root,d
28
- CREATE (d)-[r:IS_PART_OF { branch: $branch, branch_level: $branch_level, from: $now, status: $status }]->(root)
29
- RETURN %(id_func)s(r)
30
- """ % {
31
- "id_func": db.get_id_function_name(),
32
- }
33
-
34
- self.params["node_id"] = db.to_database_id(self.node_id)
35
- self.params["now"] = self.at.to_string()
36
- self.params["branch"] = self.branch.name
37
- self.params["branch_level"] = self.branch.hierarchy_level
38
- self.params["status"] = RelationshipStatus.ACTIVE.value
39
-
40
- self.add_to_query(query)
41
-
42
-
43
12
  class DeleteBranchRelationshipsQuery(Query):
44
13
  name: str = "delete_branch_relationships"
45
14
  insert_return: bool = False
@@ -52,31 +21,31 @@ class DeleteBranchRelationshipsQuery(Query):
52
21
 
53
22
  async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
54
23
  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
24
+ // delete all relationships on this branch
25
+ MATCH (s)-[r1]-(d)
26
+ WHERE r1.branch = $branch_name
27
+ CALL (r1) {
28
+ DELETE r1
29
+ } IN TRANSACTIONS
30
+
31
+ // check for any orphaned Node vertices and delete them
32
+ WITH collect(DISTINCT s.uuid) + collect(DISTINCT d.uuid) AS nodes_uuids
33
+ MATCH (s2:Node)-[r2]-(d2)
34
+ WHERE NOT exists((s2)-[:IS_PART_OF]-(:Root))
35
+ AND s2.uuid IN nodes_uuids
36
+ CALL (r2) {
37
+ DELETE r2
38
+ } IN TRANSACTIONS
39
+
40
+ // reduce results to a single row
41
+ WITH 1 AS one LIMIT 1
42
+
43
+ // find any orphaned vertices and delete them
44
+ MATCH (n)
45
+ WHERE NOT exists((n)--())
46
+ CALL (n) {
47
+ DELETE n
48
+ } IN TRANSACTIONS
80
49
  """
81
50
  self.params["branch_name"] = self.branch_name
82
51
  self.add_to_query(query)