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
@@ -6,6 +6,7 @@ from infrahub import config, lock
6
6
  from infrahub.constants.database import DatabaseType
7
7
  from infrahub.core import registry
8
8
  from infrahub.core.branch import Branch
9
+ from infrahub.core.branch.enums import BranchStatus
9
10
  from infrahub.core.constants import (
10
11
  DEFAULT_IP_NAMESPACE,
11
12
  GLOBAL_BRANCH_NAME,
@@ -224,7 +225,7 @@ async def create_root_node(db: InfrahubDatabase) -> Root:
224
225
  async def create_default_branch(db: InfrahubDatabase) -> Branch:
225
226
  branch = Branch(
226
227
  name=registry.default_branch,
227
- status="OPEN",
228
+ status=BranchStatus.OPEN,
228
229
  description="Default Branch",
229
230
  hierarchy_level=1,
230
231
  is_default=True,
@@ -241,7 +242,7 @@ async def create_default_branch(db: InfrahubDatabase) -> Branch:
241
242
  async def create_global_branch(db: InfrahubDatabase) -> Branch:
242
243
  branch = Branch(
243
244
  name=GLOBAL_BRANCH_NAME,
244
- status="OPEN",
245
+ status=BranchStatus.OPEN,
245
246
  description="Global Branch",
246
247
  hierarchy_level=1,
247
248
  is_global=True,
@@ -264,7 +265,7 @@ async def create_branch(
264
265
  description = description or f"Branch {branch_name}"
265
266
  branch = Branch(
266
267
  name=branch_name,
267
- status="OPEN",
268
+ status=BranchStatus.OPEN,
268
269
  hierarchy_level=2,
269
270
  description=description,
270
271
  is_default=False,
infrahub/core/merge.py CHANGED
@@ -9,7 +9,7 @@ from infrahub.core.models import SchemaUpdateValidationResult
9
9
  from infrahub.core.protocols import CoreRepository
10
10
  from infrahub.core.registry import registry
11
11
  from infrahub.core.timestamp import Timestamp
12
- from infrahub.exceptions import ValidationError
12
+ from infrahub.exceptions import MergeFailedError, ValidationError
13
13
  from infrahub.log import get_logger
14
14
 
15
15
  from ..git.models import GitRepositoryMerge
@@ -18,6 +18,7 @@ from ..workflows.catalogue import GIT_REPOSITORIES_MERGE
18
18
  if TYPE_CHECKING:
19
19
  from infrahub.core.branch import Branch
20
20
  from infrahub.core.diff.coordinator import DiffCoordinator
21
+ from infrahub.core.diff.diff_locker import DiffLocker
21
22
  from infrahub.core.diff.merger.merger import DiffMerger
22
23
  from infrahub.core.diff.model.path import EnrichedDiffRoot
23
24
  from infrahub.core.diff.repository.repository import DiffRepository
@@ -39,6 +40,7 @@ class BranchMerger:
39
40
  diff_coordinator: DiffCoordinator,
40
41
  diff_merger: DiffMerger,
41
42
  diff_repository: DiffRepository,
43
+ diff_locker: DiffLocker,
42
44
  destination_branch: Branch | None = None,
43
45
  service: InfrahubServices | None = None,
44
46
  ):
@@ -48,6 +50,7 @@ class BranchMerger:
48
50
  self.diff_coordinator = diff_coordinator
49
51
  self.diff_merger = diff_merger
50
52
  self.diff_repository = diff_repository
53
+ self.diff_locker = diff_locker
51
54
  self.migrations: list[SchemaUpdateMigrationInfo] = []
52
55
  self._merge_at = Timestamp()
53
56
 
@@ -185,22 +188,34 @@ class BranchMerger:
185
188
  )
186
189
  log.info("Diff updated for merge")
187
190
 
188
- errors: list[str] = []
189
- async for conflict_path, conflict in self.diff_repository.get_all_conflicts_for_diff(
190
- diff_branch_name=self.source_branch.name, tracking_id=BranchTrackingId(name=self.source_branch.name)
191
+ log.info("Acquiring lock for merge")
192
+ async with self.diff_locker.acquire_lock(
193
+ target_branch_name=self.destination_branch.name,
194
+ source_branch_name=self.source_branch.name,
195
+ is_incremental=False,
191
196
  ):
192
- if conflict.selected_branch is None or conflict.resolvable is False:
193
- errors.append(conflict_path)
194
-
195
- if errors:
196
- raise ValidationError(
197
- f"Unable to merge the branch '{self.source_branch.name}', conflict resolution missing: {', '.join(errors)}"
198
- )
199
-
200
- # TODO need to find a way to properly communicate back to the user any issue that could come up during the merge
201
- # From the Graph or From the repositories
202
- self._merge_at = Timestamp(at)
203
- branch_diff = await self.diff_merger.merge_graph(at=self._merge_at)
197
+ log.info("Lock acquired for merge")
198
+ try:
199
+ errors: list[str] = []
200
+ async for conflict_path, conflict in self.diff_repository.get_all_conflicts_for_diff(
201
+ diff_branch_name=self.source_branch.name, tracking_id=BranchTrackingId(name=self.source_branch.name)
202
+ ):
203
+ if conflict.selected_branch is None or conflict.resolvable is False:
204
+ errors.append(conflict_path)
205
+
206
+ if errors:
207
+ raise ValidationError(
208
+ f"Unable to merge the branch '{self.source_branch.name}', conflict resolution missing: {', '.join(errors)}"
209
+ )
210
+
211
+ # TODO need to find a way to properly communicate back to the user any issue that could come up during the merge
212
+ # From the Graph or From the repositories
213
+ self._merge_at = Timestamp(at)
214
+ branch_diff = await self.diff_merger.merge_graph(at=self._merge_at)
215
+ except Exception as exc:
216
+ log.exception("Merge failed, beginning rollback")
217
+ await self.rollback()
218
+ raise MergeFailedError(branch_name=self.source_branch.name) from exc
204
219
  await self.merge_repositories()
205
220
  return branch_diff
206
221
 
@@ -33,6 +33,9 @@ from .m028_delete_diffs import Migration028
33
33
  from .m029_duplicates_cleanup import Migration029
34
34
  from .m030_illegal_edges import Migration030
35
35
  from .m031_check_number_attributes import Migration031
36
+ from .m032_cleanup_orphaned_branch_relationships import Migration032
37
+ from .m033_deduplicate_relationship_vertices import Migration033
38
+ from .m034_find_orphaned_schema_fields import Migration034
36
39
 
37
40
  if TYPE_CHECKING:
38
41
  from infrahub.core.root import Root
@@ -71,6 +74,9 @@ MIGRATIONS: list[type[GraphMigration | InternalSchemaMigration | ArbitraryMigrat
71
74
  Migration029,
72
75
  Migration030,
73
76
  Migration031,
77
+ Migration032,
78
+ Migration033,
79
+ Migration034,
74
80
  ]
75
81
 
76
82
 
@@ -85,3 +91,23 @@ async def get_graph_migrations(
85
91
  applicable_migrations.append(migration)
86
92
 
87
93
  return applicable_migrations
94
+
95
+
96
+ def get_migration_by_number(
97
+ migration_number: int | str,
98
+ ) -> GraphMigration | InternalSchemaMigration | ArbitraryMigration:
99
+ # Convert to string and pad with zeros if needed
100
+ try:
101
+ num = int(migration_number)
102
+ migration_str = f"{num:03d}"
103
+ except (ValueError, TypeError) as exc:
104
+ raise ValueError(f"Invalid migration number: {migration_number}") from exc
105
+
106
+ migration_name = f"Migration{migration_str}"
107
+
108
+ # Find the migration in the MIGRATIONS list
109
+ for migration_class in MIGRATIONS:
110
+ if migration_class.__name__ == migration_name:
111
+ return migration_class.init()
112
+
113
+ raise ValueError(f"Migration {migration_number} not found")
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING, Any, Sequence
4
4
 
5
5
  from infrahub.core.branch import Branch
6
+ from infrahub.core.branch.enums import BranchStatus
6
7
  from infrahub.core.constants import GLOBAL_BRANCH_NAME, BranchSupportType, InfrahubKind
7
8
  from infrahub.core.migrations.shared import MigrationResult
8
9
  from infrahub.core.query import Query, QueryType
@@ -20,7 +21,7 @@ if TYPE_CHECKING:
20
21
 
21
22
  global_branch = Branch(
22
23
  name=GLOBAL_BRANCH_NAME,
23
- status="OPEN",
24
+ status=BranchStatus.OPEN,
24
25
  description="Global Branch",
25
26
  hierarchy_level=1,
26
27
  is_global=True,
@@ -29,7 +30,7 @@ global_branch = Branch(
29
30
 
30
31
  default_branch = Branch(
31
32
  name="main",
32
- status="OPEN",
33
+ status=BranchStatus.OPEN,
33
34
  description="Default Branch",
34
35
  hierarchy_level=1,
35
36
  is_global=False,
@@ -105,7 +106,7 @@ class Migration012AddLabelData(NodeDuplicateQuery):
105
106
 
106
107
  branch = Branch(
107
108
  name=GLOBAL_BRANCH_NAME,
108
- status="OPEN",
109
+ status=BranchStatus.OPEN,
109
110
  description="Global Branch",
110
111
  hierarchy_level=1,
111
112
  is_global=True,
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING, Any, Sequence
4
4
 
5
5
  from infrahub.core.branch import Branch
6
+ from infrahub.core.branch.enums import BranchStatus
6
7
  from infrahub.core.constants import (
7
8
  GLOBAL_BRANCH_NAME,
8
9
  BranchSupportType,
@@ -23,7 +24,7 @@ if TYPE_CHECKING:
23
24
 
24
25
  default_branch = Branch(
25
26
  name="main",
26
- status="OPEN",
27
+ status=BranchStatus.OPEN,
27
28
  description="Default Branch",
28
29
  hierarchy_level=1,
29
30
  is_global=False,
@@ -42,7 +43,7 @@ class Migration013ConvertCoreRepositoryWithCred(Query):
42
43
 
43
44
  global_branch = Branch(
44
45
  name=GLOBAL_BRANCH_NAME,
45
- status="OPEN",
46
+ status=BranchStatus.OPEN,
46
47
  description="Global Branch",
47
48
  hierarchy_level=1,
48
49
  is_global=True,
@@ -176,7 +177,7 @@ class Migration013ConvertCoreRepositoryWithoutCred(Query):
176
177
 
177
178
  global_branch = Branch(
178
179
  name=GLOBAL_BRANCH_NAME,
179
- status="OPEN",
180
+ status=BranchStatus.OPEN,
180
181
  description="Global Branch",
181
182
  hierarchy_level=1,
182
183
  is_global=True,
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from infrahub.core.migrations.shared import MigrationResult
6
+ from infrahub.core.query import Query, QueryType
7
+ from infrahub.core.query.branch import DeleteBranchRelationshipsQuery
8
+ from infrahub.log import get_logger
9
+
10
+ from ..shared import ArbitraryMigration
11
+
12
+ if TYPE_CHECKING:
13
+ from infrahub.database import InfrahubDatabase
14
+
15
+ log = get_logger()
16
+
17
+
18
+ class DeletedBranchCleanupQuery(Query):
19
+ """
20
+ Find all unique edge branch names for which there is no Branch object
21
+ """
22
+
23
+ name = "deleted_branch_cleanup"
24
+ type = QueryType.WRITE
25
+ insert_return = False
26
+
27
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
28
+ query = """
29
+ MATCH (b:Branch)
30
+ WITH collect(DISTINCT b.name) AS branch_names
31
+ MATCH ()-[e]->()
32
+ WHERE e.branch IS NOT NULL
33
+ AND NOT e.branch IN branch_names
34
+ RETURN DISTINCT (e.branch) AS branch_name
35
+ """
36
+ self.add_to_query(query)
37
+ self.return_labels = ["branch_name"]
38
+
39
+
40
+ class DeleteOrphanRelationshipsQuery(Query):
41
+ """
42
+ Find all Relationship vertices that link to fewer than 2 Node vertices and delete them
43
+ """
44
+
45
+ name = "delete_orphan_relationships"
46
+ type = QueryType.WRITE
47
+ insert_return = False
48
+
49
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
50
+ query = """
51
+ MATCH (r:Relationship)-[:IS_RELATED]-(n:Node)
52
+ WITH DISTINCT r, n
53
+ WITH r, count(*) AS node_count
54
+ WHERE node_count < 2
55
+ DETACH DELETE r
56
+ """
57
+ self.add_to_query(query)
58
+
59
+
60
+ class Migration032(ArbitraryMigration):
61
+ """
62
+ Delete edges for branches that were not completely deleted
63
+ """
64
+
65
+ name: str = "032_cleanup_deleted_branches"
66
+ minimum_version: int = 31
67
+
68
+ async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
69
+ return MigrationResult()
70
+
71
+ async def execute(self, db: InfrahubDatabase) -> MigrationResult:
72
+ migration_result = MigrationResult()
73
+
74
+ try:
75
+ log.info("Get partially deleted branch names...")
76
+ orphaned_branches_query = await DeletedBranchCleanupQuery.init(db=db)
77
+ await orphaned_branches_query.execute(db=db)
78
+
79
+ orphaned_branch_names = []
80
+ for result in orphaned_branches_query.get_results():
81
+ branch_name = result.get_as_type("branch_name", str)
82
+ orphaned_branch_names.append(branch_name)
83
+
84
+ if not orphaned_branch_names:
85
+ log.info("No partially deleted branches found. All done.")
86
+ return migration_result
87
+
88
+ log.info(f"Found {len(orphaned_branch_names)} orphaned branch names: {orphaned_branch_names}")
89
+
90
+ for branch_name in orphaned_branch_names:
91
+ log.info(f"Cleaning up branch '{branch_name}'...")
92
+ delete_query = await DeleteBranchRelationshipsQuery.init(db=db, branch_name=branch_name)
93
+ await delete_query.execute(db=db)
94
+ log.info(f"Branch '{branch_name}' cleaned up.")
95
+
96
+ log.info("Deleting orphaned relationships...")
97
+ delete_relationships_query = await DeleteOrphanRelationshipsQuery.init(db=db)
98
+ await delete_relationships_query.execute(db=db)
99
+ log.info("Orphaned relationships deleted.")
100
+
101
+ except Exception as exc:
102
+ migration_result.errors.append(str(exc))
103
+ log.exception("Error during branch cleanup")
104
+
105
+ return migration_result
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Sequence
4
+
5
+ from infrahub.core.migrations.shared import GraphMigration, MigrationResult
6
+ from infrahub.log import get_logger
7
+
8
+ from ...query import Query, QueryType
9
+
10
+ if TYPE_CHECKING:
11
+ from infrahub.database import InfrahubDatabase
12
+
13
+ log = get_logger()
14
+
15
+
16
+ class DeduplicateRelationshipVerticesQuery(Query):
17
+ """
18
+ For each group of duplicate Relationships with the same UUID, delete any Relationship that meets the following criteria:
19
+ - is linked to a deleted node (only if the delete time is before the Relationship's from time)
20
+ - is linked to a node on an incorrect branch (ie Relationship added on main, but Node is on a branch)
21
+ """
22
+
23
+ name = "deduplicate_relationship_vertices"
24
+ type = QueryType.WRITE
25
+ insert_return = False
26
+
27
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
28
+ query = """
29
+ MATCH (root:Root)
30
+ WITH root.default_branch AS default_branch_name
31
+ // ------------
32
+ // Find all Relationship vertices with duplicate UUIDs
33
+ // ------------
34
+ MATCH (r:Relationship)
35
+ WITH r.uuid AS r_uuid, default_branch_name, count(*) AS num_dups
36
+ WHERE num_dups > 1
37
+ WITH DISTINCT r_uuid, default_branch_name
38
+ // ------------
39
+ // get the branched_from time for each relationship edge and node
40
+ // ------------
41
+ MATCH (rel:Relationship {uuid: r_uuid})
42
+ CALL (rel) {
43
+ MATCH (rel)-[is_rel_e:IS_RELATED {status: "active"}]-(n:Node)
44
+ MATCH (rel_branch:Branch {name: is_rel_e.branch})
45
+ RETURN is_rel_e, rel_branch.branched_from AS rel_branched_from, n
46
+ }
47
+ // ------------
48
+ // for each IS_RELATED edge of the relationship, check the IS_PART_OF edges of the Node vertex
49
+ // to determine if this side of the relationship is legal
50
+ // ------------
51
+ CALL (n, is_rel_e, rel_branched_from, default_branch_name) {
52
+ OPTIONAL MATCH (n)-[is_part_of_e:IS_PART_OF {status: "active"}]->(:Root)
53
+ WHERE (
54
+ // the Node's create time must precede the Relationship's create time
55
+ is_part_of_e.from <= is_rel_e.from AND (is_part_of_e.to >= is_rel_e.from OR is_part_of_e.to IS NULL)
56
+ // the Node must have been created on a branch of equal or lesser depth than the Relationship
57
+ AND is_part_of_e.branch_level <= is_rel_e.branch_level
58
+ // if the Node and Relationships were created on branch_level = 2, then they must be on the same branch
59
+ AND (
60
+ is_part_of_e.branch_level = 1
61
+ OR is_part_of_e.branch = is_rel_e.branch
62
+ )
63
+ // if the Node was created on the default branch, and the Relationship was created on a branch,
64
+ // then the Node must have been created after the branched_from time of the Relationship's branch
65
+ AND (
66
+ is_part_of_e.branch <> default_branch_name
67
+ OR is_rel_e.branch_level = 1
68
+ OR is_part_of_e.from <= rel_branched_from
69
+ )
70
+ )
71
+ WITH is_part_of_e IS NOT NULL AS is_legal
72
+ ORDER BY is_legal DESC
73
+ RETURN is_legal
74
+ LIMIT 1
75
+ }
76
+ WITH rel, is_legal
77
+ ORDER BY rel, is_legal ASC
78
+ WITH rel, head(collect(is_legal)) AS is_legal
79
+ WHERE is_legal = false
80
+ DETACH DELETE rel
81
+ """
82
+ self.add_to_query(query)
83
+
84
+
85
+ class Migration033(GraphMigration):
86
+ """
87
+ Identifies duplicate Relationship vertices that have the same UUID property. Deletes any duplicates that
88
+ are linked to deleted nodes or nodes on in incorrect branch.
89
+ """
90
+
91
+ name: str = "033_deduplicate_relationship_vertices"
92
+ minimum_version: int = 31
93
+ queries: Sequence[type[Query]] = [DeduplicateRelationshipVerticesQuery]
94
+
95
+ async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
96
+ result = MigrationResult()
97
+ return result
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from typing import TYPE_CHECKING, Any, Sequence
5
+
6
+ from infrahub.core.initialization import initialization
7
+ from infrahub.core.manager import NodeManager
8
+ from infrahub.core.migrations.shared import ArbitraryMigration, MigrationResult
9
+ from infrahub.core.timestamp import Timestamp
10
+ from infrahub.lock import initialize_lock
11
+ from infrahub.log import get_logger
12
+
13
+ from ...query import Query, QueryType
14
+
15
+ if TYPE_CHECKING:
16
+ from infrahub.database import InfrahubDatabase
17
+
18
+ log = get_logger()
19
+
20
+
21
+ class FindOrphanedSchemaFieldsQuery(Query):
22
+ name = "find_orphaned_schema_fields"
23
+ type = QueryType.WRITE
24
+
25
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
26
+ query = """
27
+ // ------------
28
+ // Find orphaned SchemaRelationship and SchemaAttribute vertices
29
+ // ------------
30
+ MATCH (schema_field:SchemaRelationship|SchemaAttribute)-[e:IS_RELATED]-(rel:Relationship)
31
+ WHERE rel.name IN ["schema__node__relationships", "schema__node__attributes"]
32
+ AND e.status = "deleted" OR e.to IS NOT NULL
33
+ WITH schema_field, e.branch AS branch, CASE
34
+ WHEN e.status = "deleted" THEN e.from
35
+ ELSE e.to
36
+ END AS delete_time
37
+ CALL (schema_field, branch) {
38
+ OPTIONAL MATCH (schema_field)-[is_part_of:IS_PART_OF {branch: branch}]->(:Root)
39
+ WHERE is_part_of.status = "deleted" OR is_part_of.to IS NOT NULL
40
+ RETURN is_part_of IS NOT NULL AS is_deleted
41
+ }
42
+ WITH schema_field, branch, delete_time
43
+ WHERE is_deleted = FALSE
44
+ """
45
+ self.add_to_query(query)
46
+ self.return_labels = ["schema_field.uuid AS schema_field_uuid", "branch", "delete_time"]
47
+
48
+
49
+ class Migration034(ArbitraryMigration):
50
+ """
51
+ Finds active SchemaRelationship and SchemaAttribute vertices with deleted relationships to SchemaNodes or
52
+ SchemaGenerics and deletes them on the same branch at the same time
53
+ """
54
+
55
+ name: str = "034_find_orphaned_schema_fields"
56
+ minimum_version: int = 33
57
+ queries: Sequence[type[Query]] = [FindOrphanedSchemaFieldsQuery]
58
+
59
+ async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
60
+ return MigrationResult()
61
+
62
+ async def execute(self, db: InfrahubDatabase) -> MigrationResult:
63
+ try:
64
+ initialize_lock()
65
+ await initialization(db=db)
66
+ query = await FindOrphanedSchemaFieldsQuery.init(db=db)
67
+ await query.execute(db=db)
68
+ schema_field_uuids_by_branch: dict[str, dict[str, str]] = defaultdict(dict)
69
+ for result in query.get_results():
70
+ schema_field_uuid = result.get_as_type("schema_field_uuid", return_type=str)
71
+ branch = result.get_as_type("branch", return_type=str)
72
+ delete_time = result.get_as_type("delete_time", return_type=str)
73
+ schema_field_uuids_by_branch[branch][schema_field_uuid] = delete_time
74
+
75
+ for branch, schema_rel_details in schema_field_uuids_by_branch.items():
76
+ node_map = await NodeManager.get_many(db=db, branch=branch, ids=list(schema_rel_details.keys()))
77
+ for schema_field_uuid, orphan_schema_rel_node in node_map.items():
78
+ delete_time = Timestamp(schema_rel_details[schema_field_uuid])
79
+ await orphan_schema_rel_node.delete(db=db, at=delete_time)
80
+ except Exception as exc:
81
+ log.exception("Error during orphaned schema field cleanup")
82
+ return MigrationResult(errors=[str(exc)])
83
+
84
+ return MigrationResult()
@@ -1,10 +1,22 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any, Sequence
3
+ from typing import TYPE_CHECKING, Any, Sequence
4
+
5
+ from infrahub.core import registry
6
+ from infrahub.core.node import Node
7
+ from infrahub.exceptions import PoolExhaustedError
8
+ from infrahub.tasks.registry import update_branch_registry
4
9
 
5
10
  from ..query import AttributeMigrationQuery
6
11
  from ..query.attribute_add import AttributeAddQuery
7
- from ..shared import AttributeSchemaMigration
12
+ from ..shared import AttributeSchemaMigration, MigrationResult
13
+
14
+ if TYPE_CHECKING:
15
+ from infrahub.core.node.resource_manager.number_pool import CoreNumberPool
16
+ from infrahub.database import InfrahubDatabase
17
+
18
+ from ...branch import Branch
19
+ from ...timestamp import Timestamp
8
20
 
9
21
 
10
22
  class NodeAttributeAddMigrationQuery01(AttributeMigrationQuery, AttributeAddQuery):
@@ -29,3 +41,44 @@ class NodeAttributeAddMigrationQuery01(AttributeMigrationQuery, AttributeAddQuer
29
41
  class NodeAttributeAddMigration(AttributeSchemaMigration):
30
42
  name: str = "node.attribute.add"
31
43
  queries: Sequence[type[AttributeMigrationQuery]] = [NodeAttributeAddMigrationQuery01] # type: ignore[assignment]
44
+
45
+ async def execute_post_queries(
46
+ self,
47
+ db: InfrahubDatabase,
48
+ result: MigrationResult,
49
+ branch: Branch,
50
+ at: Timestamp, # noqa: ARG002
51
+ ) -> MigrationResult:
52
+ if self.new_attribute_schema.kind != "NumberPool":
53
+ return result
54
+
55
+ number_pool: CoreNumberPool = await Node.fetch_or_create_number_pool( # type: ignore[assignment]
56
+ db=db, branch=branch, schema_node=self.new_schema, schema_attribute=self.new_attribute_schema
57
+ )
58
+
59
+ await update_branch_registry(db=db, branch=branch)
60
+
61
+ nodes: list[Node] = await registry.manager.query(
62
+ db=db, branch=branch, schema=self.new_schema, fields={"id": True, self.new_attribute_schema.name: True}
63
+ )
64
+
65
+ try:
66
+ numbers = await number_pool.get_next_many(
67
+ db=db,
68
+ branch=branch,
69
+ quantity=len(nodes),
70
+ attribute=self.new_attribute_schema,
71
+ )
72
+ except PoolExhaustedError as exc:
73
+ result.errors.append(str(exc))
74
+ return result
75
+
76
+ for node, number in zip(nodes, numbers, strict=True):
77
+ await number_pool.reserve(db=db, number=number, identifier=node.get_id())
78
+ attr = getattr(node, self.new_attribute_schema.name)
79
+ attr.value = number
80
+ attr.source = number_pool.id
81
+
82
+ await node.save(db=db, fields=[self.new_attribute_schema.name])
83
+
84
+ return result
@@ -16,13 +16,13 @@ from infrahub.core.schema import (
16
16
  SchemaRoot,
17
17
  internal_schema,
18
18
  )
19
+ from infrahub.core.timestamp import Timestamp
19
20
 
20
21
  from .query import MigrationQuery # noqa: TC001
21
22
 
22
23
  if TYPE_CHECKING:
23
24
  from infrahub.core.branch import Branch
24
25
  from infrahub.core.schema.schema_branch import SchemaBranch
25
- from infrahub.core.timestamp import Timestamp
26
26
  from infrahub.database import InfrahubDatabase
27
27
 
28
28
 
@@ -47,18 +47,46 @@ class SchemaMigration(BaseModel):
47
47
  previous_node_schema: NodeSchema | GenericSchema | None = None
48
48
  schema_path: SchemaPath
49
49
 
50
+ async def execute_pre_queries(
51
+ self,
52
+ db: InfrahubDatabase, # noqa: ARG002
53
+ result: MigrationResult,
54
+ branch: Branch, # noqa: ARG002
55
+ at: Timestamp, # noqa: ARG002
56
+ ) -> MigrationResult:
57
+ return result
58
+
59
+ async def execute_post_queries(
60
+ self,
61
+ db: InfrahubDatabase, # noqa: ARG002
62
+ result: MigrationResult,
63
+ branch: Branch, # noqa: ARG002
64
+ at: Timestamp, # noqa: ARG002
65
+ ) -> MigrationResult:
66
+ return result
67
+
68
+ async def execute_queries(
69
+ self, db: InfrahubDatabase, result: MigrationResult, branch: Branch, at: Timestamp
70
+ ) -> MigrationResult:
71
+ for migration_query in self.queries:
72
+ try:
73
+ query = await migration_query.init(db=db, branch=branch, at=at, migration=self)
74
+ await query.execute(db=db)
75
+ result.nbr_migrations_executed += query.get_nbr_migrations_executed()
76
+ except Exception as exc:
77
+ result.errors.append(str(exc))
78
+ return result
79
+
80
+ return result
81
+
50
82
  async def execute(self, db: InfrahubDatabase, branch: Branch, at: Timestamp | str | None = None) -> MigrationResult:
51
83
  async with db.start_transaction() as ts:
52
84
  result = MigrationResult()
85
+ at = Timestamp(at)
53
86
 
54
- for migration_query in self.queries:
55
- try:
56
- query = await migration_query.init(db=ts, branch=branch, at=at, migration=self)
57
- await query.execute(db=ts)
58
- result.nbr_migrations_executed += query.get_nbr_migrations_executed()
59
- except Exception as exc:
60
- result.errors.append(str(exc))
61
- return result
87
+ await self.execute_pre_queries(db=ts, result=result, branch=branch, at=at)
88
+ await self.execute_queries(db=ts, result=result, branch=branch, at=at)
89
+ await self.execute_post_queries(db=ts, result=result, branch=branch, at=at)
62
90
 
63
91
  return result
64
92