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
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any, AsyncIterator, Generator
10
10
  from infrahub import config
11
11
  from infrahub.core import registry
12
12
  from infrahub.core.constants import (
13
+ GLOBAL_BRANCH_NAME,
13
14
  AttributeDBNodeType,
14
15
  RelationshipDirection,
15
16
  RelationshipHierarchyDirection,
@@ -128,7 +129,7 @@ class NodeCreateAllQuery(NodeQuery):
128
129
 
129
130
  raise_error_if_empty: bool = True
130
131
 
131
- async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
132
+ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002, PLR0915
132
133
  at = self.at or self.node._at
133
134
  self.params["uuid"] = self.node.id
134
135
  self.params["branch"] = self.branch.name
@@ -151,11 +152,19 @@ class NodeCreateAllQuery(NodeQuery):
151
152
  else:
152
153
  attributes.append(attr_data)
153
154
 
155
+ deepest_branch_name = self.branch.name
156
+ deepest_branch_level = self.branch.hierarchy_level
154
157
  relationships: list[RelationshipCreateData] = []
155
158
  for rel_name in self.node._relationships:
156
159
  rel_manager: RelationshipManager = getattr(self.node, rel_name)
157
160
  for rel in rel_manager._relationships:
158
- relationships.append(await rel.get_create_data(db=db))
161
+ rel_create_data = await rel.get_create_data(db=db, at=at)
162
+ if rel_create_data.peer_branch_level > deepest_branch_level or (
163
+ deepest_branch_name == GLOBAL_BRANCH_NAME and rel_create_data.peer_branch == registry.default_branch
164
+ ):
165
+ deepest_branch_name = rel_create_data.peer_branch
166
+ deepest_branch_level = rel_create_data.peer_branch_level
167
+ relationships.append(rel_create_data)
159
168
 
160
169
  self.params["attrs"] = [attr.model_dump() for attr in attributes]
161
170
  self.params["attrs_iphost"] = [attr.model_dump() for attr in attributes_iphost]
@@ -216,6 +225,8 @@ class NodeCreateAllQuery(NodeQuery):
216
225
  CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
217
226
  MERGE (ip:Boolean { value: attr.is_protected })
218
227
  MERGE (iv:Boolean { value: attr.is_visible })
228
+ WITH a, ip, iv
229
+ LIMIT 1
219
230
  CREATE (a)-[:IS_PROTECTED { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(ip)
220
231
  CREATE (a)-[:IS_VISIBLE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(iv)
221
232
  FOREACH ( prop IN attr.source_prop |
@@ -230,9 +241,8 @@ class NodeCreateAllQuery(NodeQuery):
230
241
 
231
242
  attrs_iphost_query = """
232
243
  WITH distinct n
233
- UNWIND $attrs_iphost AS attr_iphost
234
- CALL (n, attr_iphost) {
235
- WITH n, attr_iphost AS attr
244
+ UNWIND $attrs_iphost AS attr
245
+ CALL (n, attr) {
236
246
  CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
237
247
  CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
238
248
  MERGE (av:AttributeValue:AttributeIPHost { %(iphost_prop)s })
@@ -241,6 +251,8 @@ class NodeCreateAllQuery(NodeQuery):
241
251
  CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
242
252
  MERGE (ip:Boolean { value: attr.is_protected })
243
253
  MERGE (iv:Boolean { value: attr.is_visible })
254
+ WITH a, ip, iv
255
+ LIMIT 1
244
256
  CREATE (a)-[:IS_PROTECTED { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(ip)
245
257
  CREATE (a)-[:IS_VISIBLE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(iv)
246
258
  FOREACH ( prop IN attr.source_prop |
@@ -256,9 +268,8 @@ class NodeCreateAllQuery(NodeQuery):
256
268
 
257
269
  attrs_ipnetwork_query = """
258
270
  WITH distinct n
259
- UNWIND $attrs_ipnetwork AS attr_ipnetwork
260
- CALL (n, attr_ipnetwork) {
261
- WITH n, attr_ipnetwork AS attr
271
+ UNWIND $attrs_ipnetwork AS attr
272
+ CALL (n, attr) {
262
273
  CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
263
274
  CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
264
275
  MERGE (av:AttributeValue:AttributeIPNetwork { %(ipnetwork_prop)s })
@@ -267,6 +278,8 @@ class NodeCreateAllQuery(NodeQuery):
267
278
  CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
268
279
  MERGE (ip:Boolean { value: attr.is_protected })
269
280
  MERGE (iv:Boolean { value: attr.is_visible })
281
+ WITH a, ip, iv
282
+ LIMIT 1
270
283
  CREATE (a)-[:IS_PROTECTED { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(ip)
271
284
  CREATE (a)-[:IS_VISIBLE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(iv)
272
285
  FOREACH ( prop IN attr.source_prop |
@@ -280,16 +293,55 @@ class NodeCreateAllQuery(NodeQuery):
280
293
  }
281
294
  """ % {"ipnetwork_prop": ", ".join(ipnetwork_prop_list)}
282
295
 
296
+ deepest_branch = await registry.get_branch(db=db, branch=deepest_branch_name)
297
+ branch_filter, branch_params = deepest_branch.get_query_filter_path(at=self.at)
298
+ self.params.update(branch_params)
299
+ self.params["global_branch_name"] = GLOBAL_BRANCH_NAME
300
+ self.params["default_branch_name"] = registry.default_branch
301
+
302
+ dest_node_subquery = """
303
+ CALL (rel) {
304
+ MATCH (dest_node:Node { uuid: rel.destination_id })-[r:IS_PART_OF]->(root:Root)
305
+ WHERE (
306
+ // if the relationship is on a branch, use the regular filter
307
+ (rel.peer_branch_level = 2 AND %(branch_filter)s)
308
+ // simplified filter for the global branch
309
+ OR (
310
+ rel.peer_branch_level = 1
311
+ AND rel.peer_branch = $global_branch_name
312
+ AND r.branch = $global_branch_name
313
+ AND r.from <= $at AND (r.to IS NULL or r.to > $at)
314
+ )
315
+ // simplified filter for the default branch
316
+ OR (
317
+ rel.peer_branch_level = 1 AND
318
+ rel.peer_branch = $default_branch_name AND
319
+ r.branch IN [$default_branch_name, $global_branch_name]
320
+ AND r.from <= $at AND (r.to IS NULL or r.to > $at)
321
+ )
322
+ )
323
+ // r.status is a tie-breaker when there are nodes with the same UUID added/deleted at the same time
324
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
325
+ WITH dest_node, r
326
+ LIMIT 1
327
+ WITH dest_node, r
328
+ WHERE r.status = "active"
329
+ RETURN dest_node
330
+ }
331
+ """ % {"branch_filter": branch_filter}
332
+
283
333
  rels_bidir_query = """
284
334
  WITH distinct n
285
335
  UNWIND $rels_bidir AS rel
286
- CALL (n, rel) {
287
- MERGE (d:Node { uuid: rel.destination_id })
336
+ %(dest_node_subquery)s
337
+ CALL (n, rel, dest_node) {
288
338
  CREATE (rl:Relationship { uuid: rel.uuid, name: rel.name, branch_support: rel.branch_support })
289
339
  CREATE (n)-[:IS_RELATED %(rel_prop)s ]->(rl)
290
- CREATE (d)-[:IS_RELATED %(rel_prop)s ]->(rl)
340
+ CREATE (dest_node)-[:IS_RELATED %(rel_prop)s ]->(rl)
291
341
  MERGE (ip:Boolean { value: rel.is_protected })
292
342
  MERGE (iv:Boolean { value: rel.is_visible })
343
+ WITH rl, ip, iv
344
+ LIMIT 1
293
345
  CREATE (rl)-[:IS_PROTECTED { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(ip)
294
346
  CREATE (rl)-[:IS_VISIBLE { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(iv)
295
347
  FOREACH ( prop IN rel.source_prop |
@@ -301,19 +353,20 @@ class NodeCreateAllQuery(NodeQuery):
301
353
  CREATE (rl)-[:HAS_OWNER { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(peer)
302
354
  )
303
355
  }
304
- """ % {"rel_prop": rel_prop_str}
356
+ """ % {"rel_prop": rel_prop_str, "dest_node_subquery": dest_node_subquery}
305
357
 
306
358
  rels_out_query = """
307
359
  WITH distinct n
308
- UNWIND $rels_out AS rel_out
309
- CALL (n, rel_out) {
310
- WITH n, rel_out as rel
311
- MERGE (d:Node { uuid: rel.destination_id })
360
+ UNWIND $rels_out AS rel
361
+ %(dest_node_subquery)s
362
+ CALL (n, rel, dest_node) {
312
363
  CREATE (rl:Relationship { uuid: rel.uuid, name: rel.name, branch_support: rel.branch_support })
313
364
  CREATE (n)-[:IS_RELATED %(rel_prop)s ]->(rl)
314
- CREATE (d)<-[:IS_RELATED %(rel_prop)s ]-(rl)
365
+ CREATE (dest_node)<-[:IS_RELATED %(rel_prop)s ]-(rl)
315
366
  MERGE (ip:Boolean { value: rel.is_protected })
316
367
  MERGE (iv:Boolean { value: rel.is_visible })
368
+ WITH rl, ip, iv
369
+ LIMIT 1
317
370
  CREATE (rl)-[:IS_PROTECTED { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(ip)
318
371
  CREATE (rl)-[:IS_VISIBLE { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(iv)
319
372
  FOREACH ( prop IN rel.source_prop |
@@ -325,19 +378,20 @@ class NodeCreateAllQuery(NodeQuery):
325
378
  CREATE (rl)-[:HAS_OWNER { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(peer)
326
379
  )
327
380
  }
328
- """ % {"rel_prop": rel_prop_str}
381
+ """ % {"rel_prop": rel_prop_str, "dest_node_subquery": dest_node_subquery}
329
382
 
330
383
  rels_in_query = """
331
384
  WITH distinct n
332
- UNWIND $rels_in AS rel_in
333
- CALL (n, rel_in) {
334
- WITH n, rel_in AS rel
335
- MERGE (d:Node { uuid: rel.destination_id })
385
+ UNWIND $rels_in AS rel
386
+ %(dest_node_subquery)s
387
+ CALL (n, rel, dest_node) {
336
388
  CREATE (rl:Relationship { uuid: rel.uuid, name: rel.name, branch_support: rel.branch_support })
337
389
  CREATE (n)<-[:IS_RELATED %(rel_prop)s ]-(rl)
338
- CREATE (d)-[:IS_RELATED %(rel_prop)s ]->(rl)
390
+ CREATE (dest_node)-[:IS_RELATED %(rel_prop)s ]->(rl)
339
391
  MERGE (ip:Boolean { value: rel.is_protected })
340
392
  MERGE (iv:Boolean { value: rel.is_visible })
393
+ WITH rl, ip, iv
394
+ LIMIT 1
341
395
  CREATE (rl)-[:IS_PROTECTED { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(ip)
342
396
  CREATE (rl)-[:IS_VISIBLE { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(iv)
343
397
  FOREACH ( prop IN rel.source_prop |
@@ -349,7 +403,7 @@ class NodeCreateAllQuery(NodeQuery):
349
403
  CREATE (rl)-[:HAS_OWNER { branch: rel.branch, branch_level: rel.branch_level, status: rel.status, from: $at }]->(peer)
350
404
  )
351
405
  }
352
- """ % {"rel_prop": rel_prop_str}
406
+ """ % {"rel_prop": rel_prop_str, "dest_node_subquery": dest_node_subquery}
353
407
 
354
408
  query = f"""
355
409
  MATCH (root:Root)
@@ -206,16 +206,18 @@ class RelationshipQuery(Query):
206
206
  self.params["source_id"] = self.source_id or self.source.get_id()
207
207
  if source_branch.is_global or source_branch.is_default:
208
208
  source_query_match = """
209
- MATCH (s:Node { uuid: $source_id })
209
+ MATCH (s:Node { uuid: $source_id })-[source_e:IS_PART_OF {branch: $source_branch, status: "active"}]->(:Root)
210
+ WHERE source_e.from <= $at AND (source_e.to IS NULL OR source_e.to > $at)
210
211
  OPTIONAL MATCH (s)-[delete_edge:IS_PART_OF {status: "deleted", branch: $source_branch}]->(:Root)
211
212
  WHERE delete_edge.from <= $at
212
213
  WITH *, s WHERE delete_edge IS NULL
213
214
  """
214
215
  self.params["source_branch"] = source_branch.name
215
- source_filter, source_filter_params = source_branch.get_query_filter_path(
216
- at=self.at, variable_name="r", params_prefix="src_"
217
- )
218
- source_query_match = """
216
+ else:
217
+ source_filter, source_filter_params = source_branch.get_query_filter_path(
218
+ at=self.at, variable_name="r", params_prefix="src_"
219
+ )
220
+ source_query_match = """
219
221
  MATCH (s:Node { uuid: $source_id })
220
222
  CALL (s) {
221
223
  MATCH (s)-[r:IS_PART_OF]->(:Root)
@@ -225,15 +227,16 @@ class RelationshipQuery(Query):
225
227
  LIMIT 1
226
228
  }
227
229
  WITH *, s WHERE s_is_active = TRUE
228
- """ % {"source_filter": source_filter}
229
- self.params.update(source_filter_params)
230
+ """ % {"source_filter": source_filter}
231
+ self.params.update(source_filter_params)
230
232
  self.add_to_query(source_query_match)
231
233
 
232
234
  def add_dest_match_to_query(self, destination_branch: Branch, destination_id: str) -> None:
233
235
  self.params["destination_id"] = destination_id
234
236
  if destination_branch.is_global or destination_branch.is_default:
235
237
  destination_query_match = """
236
- MATCH (d:Node { uuid: $destination_id })
238
+ MATCH (d:Node { uuid: $destination_id })-[dest_e:IS_PART_OF {branch: $destination_branch, status: "active"}]->(:Root)
239
+ WHERE dest_e.from <= $at AND (dest_e.to IS NULL OR dest_e.to > $at)
237
240
  OPTIONAL MATCH (d)-[delete_edge:IS_PART_OF {status: "deleted", branch: $destination_branch}]->(:Root)
238
241
  WHERE delete_edge.from <= $at
239
242
  WITH *, d WHERE delete_edge IS NULL
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Any
3
+ from typing import TYPE_CHECKING, Any, Generator
4
+
5
+ from pydantic import BaseModel, ConfigDict
4
6
 
5
7
  from infrahub.core import registry
6
8
  from infrahub.core.constants import InfrahubKind, RelationshipStatus
@@ -11,6 +13,13 @@ if TYPE_CHECKING:
11
13
  from infrahub.database import InfrahubDatabase
12
14
 
13
15
 
16
+ class NumberPoolIdentifierData(BaseModel):
17
+ model_config = ConfigDict(frozen=True)
18
+
19
+ value: int
20
+ identifier: str
21
+
22
+
14
23
  class IPAddressPoolGetIdentifiers(Query):
15
24
  name = "ipaddresspool_get_identifiers"
16
25
  type = QueryType.READ
@@ -158,7 +167,7 @@ class NumberPoolGetReserved(Query):
158
167
  def __init__(
159
168
  self,
160
169
  pool_id: str,
161
- identifier: str,
170
+ identifier: str | None = None,
162
171
  **kwargs: dict[str, Any],
163
172
  ) -> None:
164
173
  self.pool_id = pool_id
@@ -176,26 +185,104 @@ class NumberPoolGetReserved(Query):
176
185
 
177
186
  self.params.update(branch_params)
178
187
 
188
+ # If identifier is not provided, we return all reservations for the pool
189
+ identifier_filter = ""
190
+ if self.identifier:
191
+ identifier_filter = "r.identifier = $identifier AND "
192
+ self.params["identifier"] = self.identifier
193
+
179
194
  query = """
180
195
  MATCH (pool:%(number_pool)s { uuid: $pool_id })-[r:IS_RESERVED]->(reservation:AttributeValue)
181
196
  WHERE
182
- r.identifier = $identifier
183
- AND
197
+ %(identifier_filter)s
184
198
  %(branch_filter)s
185
- """ % {"branch_filter": branch_filter, "number_pool": InfrahubKind.NUMBERPOOL}
199
+ """ % {
200
+ "branch_filter": branch_filter,
201
+ "number_pool": InfrahubKind.NUMBERPOOL,
202
+ "identifier_filter": identifier_filter,
203
+ }
186
204
  self.add_to_query(query)
187
- self.return_labels = ["reservation.value"]
205
+ self.return_labels = ["reservation.value AS value", "r.identifier AS identifier"]
188
206
 
189
207
  def get_reservation(self) -> int | None:
190
208
  result = self.get_result()
191
209
  if result:
192
- return result.get_as_optional_type("reservation.value", return_type=int)
210
+ return result.get_as_optional_type("value", return_type=int)
193
211
  return None
194
212
 
213
+ def get_reservations(self) -> Generator[NumberPoolIdentifierData]:
214
+ for result in self.results:
215
+ yield NumberPoolIdentifierData.model_construct(
216
+ value=result.get_as_type("value", return_type=int),
217
+ identifier=result.get_as_type("identifier", return_type=str),
218
+ )
219
+
220
+
221
+ class PoolChangeReserved(Query):
222
+ """Change the identifier on all pools.
223
+ This is useful when a node is being converted to a different type and its ID has changed
224
+ """
225
+
226
+ name = "pool_change_reserved"
227
+ type = QueryType.WRITE
228
+
229
+ def __init__(
230
+ self,
231
+ existing_identifier: str,
232
+ new_identifier: str,
233
+ **kwargs: dict[str, Any],
234
+ ) -> None:
235
+ self.existing_identifier = existing_identifier
236
+ self.new_identifier = new_identifier
237
+
238
+ super().__init__(**kwargs) # type: ignore[arg-type]
239
+
240
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
241
+ self.params["new_identifier"] = self.new_identifier
242
+ self.params["existing_identifier"] = self.existing_identifier
243
+ self.params["at"] = self.at.to_string()
244
+
245
+ branch_filter, branch_params = self.branch.get_query_filter_path(
246
+ at=self.at.to_string(), branch_agnostic=self.branch_agnostic
247
+ )
248
+
249
+ self.params.update(branch_params)
250
+
251
+ global_branch = registry.get_global_branch()
252
+ self.params["rel_prop"] = {
253
+ "branch": global_branch.name,
254
+ "branch_level": global_branch.hierarchy_level,
255
+ "status": RelationshipStatus.ACTIVE.value,
256
+ "from": self.at.to_string(),
257
+ "identifier": self.new_identifier,
258
+ }
259
+
260
+ query = """
261
+ MATCH (pool:Node)-[r:IS_RESERVED]->(resource)
262
+ WHERE
263
+ r.identifier = $existing_identifier
264
+ AND
265
+ %(branch_filter)s
266
+ SET r.to = $at
267
+ CREATE (pool)-[new_rel:IS_RESERVED $rel_prop]->(resource)
268
+ """ % {"branch_filter": branch_filter}
269
+ self.add_to_query(query)
270
+ self.return_labels = ["pool.uuid AS pool_id", "r", "new_rel"]
271
+
272
+
273
+ """
274
+ Important!: The relationship IS_RESERVED for Number is not being cleaned up when the node or the branch is deleted
275
+ I think this is something we should address in the future.
276
+ It works for now because the query has been updated to match the identifier in IS_RESERVED with the UUID of the related node
277
+ But in the future, if we need to use an identifier that is not the UUID, we will need to clean up the relationships
278
+ This will be especially important as we want to support upsert with NumberPool
279
+ """
280
+
195
281
 
196
282
  class NumberPoolGetUsed(Query):
197
283
  name = "number_pool_get_used"
198
284
  type = QueryType.READ
285
+ return_model = NumberPoolIdentifierData
199
286
 
200
287
  def __init__(
201
288
  self,
@@ -219,26 +306,36 @@ class NumberPoolGetUsed(Query):
219
306
  self.params["attribute_name"] = self.pool.node_attribute.value
220
307
 
221
308
  query = """
222
- MATCH (pool:%(number_pool)s { uuid: $pool_id })
223
- CALL (pool) {
224
- MATCH (pool)-[res:IS_RESERVED]->(av:AttributeValue)<-[hv:HAS_VALUE]-(attr:Attribute)
309
+ MATCH (pool:%(number_pool)s { uuid: $pool_id })-[res:IS_RESERVED]->(av:AttributeValue)
310
+ WHERE toInteger(av.value) >= $start_range and toInteger(av.value) <= $end_range
311
+ CALL (pool, res, av) {
312
+ MATCH (pool)-[res]->(av)<-[hv:HAS_VALUE]-(attr:Attribute)<-[ha:HAS_ATTRIBUTE]-(n:%(node)s)
225
313
  WHERE
226
- attr.name = $attribute_name
227
- AND
228
- toInteger(av.value) >= $start_range and toInteger(av.value) <= $end_range
229
- AND
230
- all(r in [res, hv] WHERE (%(branch_filter)s))
231
- RETURN av, (res.status = "active" AND hv.status = "active") AS is_active
314
+ n.uuid = res.identifier AND
315
+ attr.name = $attribute_name AND
316
+ all(r in [res, hv, ha] WHERE (%(branch_filter)s))
317
+ ORDER BY res.branch_level DESC, hv.branch_level DESC, ha.branch_level DESC, res.from DESC, hv.from DESC, ha.from DESC
318
+ RETURN (res.status = "active" AND hv.status = "active" AND ha.status = "active") AS is_active
319
+ LIMIT 1
232
320
  }
233
- WITH av, is_active
234
- WHERE is_active = TRUE
321
+ WITH av, res, is_active
322
+ WHERE is_active = True
235
323
  """ % {
236
324
  "branch_filter": branch_filter,
237
325
  "number_pool": InfrahubKind.NUMBERPOOL,
326
+ "node": self.pool.node.value,
238
327
  }
328
+
239
329
  self.add_to_query(query)
240
- self.return_labels = ["av.value"]
241
- self.order_by = ["av.value"]
330
+ self.return_labels = ["DISTINCT(av.value) as value", "res.identifier as identifier"]
331
+ self.order_by = ["value"]
332
+
333
+ def iter_results(self) -> Generator[NumberPoolIdentifierData]:
334
+ for result in self.results:
335
+ yield self.return_model.model_construct(
336
+ value=result.get_as_type("value", return_type=int),
337
+ identifier=result.get_as_type("identifier", return_type=str),
338
+ )
242
339
 
243
340
 
244
341
  class NumberPoolSetReserved(Query):
@@ -63,9 +63,11 @@ class RelationshipCreateData(BaseModel):
63
63
  uuid: str
64
64
  name: str
65
65
  destination_id: str
66
- branch: str | None = None
66
+ branch: str
67
67
  branch_level: int
68
68
  branch_support: str | None = None
69
+ peer_branch: str
70
+ peer_branch_level: int
69
71
  direction: str
70
72
  status: str
71
73
  is_protected: bool
@@ -420,7 +422,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
420
422
  )
421
423
  await delete_query.execute(db=db)
422
424
 
423
- async def resolve(self, db: InfrahubDatabase) -> None:
425
+ async def resolve(self, db: InfrahubDatabase, at: Timestamp | None = None) -> None:
424
426
  """Resolve the peer of the relationship."""
425
427
 
426
428
  if self._peer is not None:
@@ -470,7 +472,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
470
472
  if hfid_str:
471
473
  data_from_pool["identifier"] = f"hfid={hfid_str} rel={self.name}"
472
474
 
473
- assigned_peer: Node = await pool.get_resource(db=db, branch=self.branch, **data_from_pool) # type: ignore[attr-defined]
475
+ assigned_peer: Node = await pool.get_resource(db=db, branch=self.branch, at=at, **data_from_pool) # type: ignore[attr-defined]
474
476
  await self.set_peer(value=assigned_peer)
475
477
  self.set_source(value=pool.id)
476
478
 
@@ -526,16 +528,19 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
526
528
 
527
529
  return response
528
530
 
529
- async def get_create_data(self, db: InfrahubDatabase) -> RelationshipCreateData:
531
+ async def get_create_data(self, db: InfrahubDatabase, at: Timestamp | None = None) -> RelationshipCreateData:
530
532
  branch = self.get_branch_based_on_support_type()
531
533
 
532
- await self.resolve(db=db)
534
+ await self.resolve(db=db, at=at)
533
535
 
534
536
  peer = await self.get_peer(db=db)
537
+ peer_branch = peer.get_branch()
535
538
  data = RelationshipCreateData(
536
539
  uuid=str(UUIDT()),
537
540
  name=self.schema.get_identifier(),
538
541
  branch=branch.name,
542
+ peer_branch=peer_branch.name,
543
+ peer_branch_level=peer_branch.hierarchy_level,
539
544
  destination_id=peer.id,
540
545
  status="active",
541
546
  direction=self.schema.direction.value,
@@ -46,6 +46,7 @@ class SchemaExtension(HashableModel):
46
46
 
47
47
  class SchemaRoot(BaseModel):
48
48
  model_config = ConfigDict(extra="forbid")
49
+
49
50
  version: str | None = Field(default=None)
50
51
  generics: list[GenericSchema] = Field(default_factory=list)
51
52
  nodes: list[NodeSchema] = Field(default_factory=list)
@@ -93,6 +94,10 @@ class SchemaRoot(BaseModel):
93
94
  """Return a new `SchemaRoot` after merging `self` with `schema`."""
94
95
  return SchemaRoot.model_validate(deep_merge_dict(dicta=self.model_dump(), dictb=schema.model_dump()))
95
96
 
97
+ def duplicate(self) -> SchemaRoot:
98
+ """Return a duplicate of the current schema."""
99
+ return SchemaRoot.model_validate(self.model_dump())
100
+
96
101
 
97
102
  internal_schema = internal.to_dict()
98
103
 
@@ -165,3 +165,9 @@ class NumberPoolParameters(AttributeParameters):
165
165
  if self.start_range > self.end_range:
166
166
  raise ValueError("`start_range` can't be less than `end_range`")
167
167
  return self
168
+
169
+ def get_pool_size(self) -> int:
170
+ """
171
+ Returns the size of the pool based on the defined ranges.
172
+ """
173
+ return self.end_range - self.start_range + 1
@@ -10,6 +10,7 @@ from infrahub import config
10
10
  from infrahub.core.constants.schema import UpdateSupport
11
11
  from infrahub.core.enums import generate_python_enum
12
12
  from infrahub.core.query.attribute import default_attribute_query_filter
13
+ from infrahub.exceptions import InitializationError
13
14
  from infrahub.types import ATTRIBUTE_KIND_LABELS, ATTRIBUTE_TYPES
14
15
 
15
16
  from .attribute_parameters import (
@@ -67,6 +68,11 @@ class AttributeSchema(GeneratedAttributeSchema):
67
68
  def is_deprecated(self) -> bool:
68
69
  return bool(self.deprecation)
69
70
 
71
+ def get_id(self) -> str:
72
+ if self.id is None:
73
+ raise InitializationError("The attribute schema has not been saved yet and doesn't have an id")
74
+ return self.id
75
+
70
76
  def to_dict(self) -> dict:
71
77
  data = self.model_dump(exclude_unset=True, exclude_none=True)
72
78
  for field_name, value in data.items():
@@ -535,7 +535,7 @@ class SchemaManager(NodeManager):
535
535
  """Delete the node with its attributes and relationships."""
536
536
  branch = await registry.get_branch(branch=branch, db=db)
537
537
 
538
- obj = await self.get_one(id=node.get_id(), branch=branch, db=db)
538
+ obj = await self.get_one(id=node.get_id(), branch=branch, db=db, prefetch_relationships=True)
539
539
  if not obj:
540
540
  raise SchemaNotFoundError(
541
541
  branch_name=branch.name,
@@ -544,16 +544,10 @@ class SchemaManager(NodeManager):
544
544
  )
545
545
 
546
546
  # First delete the attributes and the relationships
547
- items = await self.get_many(
548
- ids=[item.id for item in node.local_attributes + node.local_relationships if item.id],
549
- db=db,
550
- branch=branch,
551
- include_owner=True,
552
- include_source=True,
553
- )
554
-
555
- for item in items.values():
556
- await item.delete(db=db)
547
+ for attr_schema_node in (await obj.attributes.get_peers(db=db)).values():
548
+ await attr_schema_node.delete(db=db)
549
+ for rel_schema_node in (await obj.relationships.get_peers(db=db)).values():
550
+ await rel_schema_node.delete(db=db)
557
551
 
558
552
  await obj.delete(db=db)
559
553
 
@@ -9,6 +9,7 @@ from infrahub import config
9
9
  from infrahub.core.constants import RelationshipDirection
10
10
  from infrahub.core.query import QueryNode, QueryRel, QueryRelDirection
11
11
  from infrahub.core.relationship import Relationship
12
+ from infrahub.exceptions import InitializationError
12
13
 
13
14
  from .generated.relationship_schema import GeneratedRelationshipSchema
14
15
 
@@ -57,6 +58,11 @@ class RelationshipSchema(GeneratedRelationshipSchema):
57
58
  raise ValueError("RelationshipSchema is not initialized")
58
59
  return self.identifier
59
60
 
61
+ def get_id(self) -> str:
62
+ if not self.id:
63
+ raise InitializationError("The relationship schema has not been saved yet and doesn't have an id")
64
+ return self.id
65
+
60
66
  def get_query_arrows(self) -> QueryArrows:
61
67
  """Return (in 4 parts) the 2 arrows for the relationship R1 and R2 based on the direction of the relationship."""
62
68