infrahub-server 1.3.0b5__py3-none-any.whl → 1.3.1__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 (68) hide show
  1. infrahub/actions/constants.py +36 -79
  2. infrahub/actions/schema.py +2 -0
  3. infrahub/cli/db.py +7 -5
  4. infrahub/cli/upgrade.py +6 -1
  5. infrahub/core/attribute.py +5 -0
  6. infrahub/core/constraint/node/runner.py +3 -1
  7. infrahub/core/convert_object_type/conversion.py +2 -0
  8. infrahub/core/diff/coordinator.py +8 -1
  9. infrahub/core/diff/query/delete_query.py +8 -4
  10. infrahub/core/diff/query/field_specifiers.py +1 -1
  11. infrahub/core/diff/query/merge.py +2 -2
  12. infrahub/core/diff/repository/repository.py +4 -0
  13. infrahub/core/graph/__init__.py +1 -1
  14. infrahub/core/migrations/graph/__init__.py +2 -0
  15. infrahub/core/migrations/graph/m012_convert_account_generic.py +1 -1
  16. infrahub/core/migrations/graph/m015_diff_format_update.py +1 -2
  17. infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +1 -2
  18. infrahub/core/migrations/graph/m023_deduplicate_cardinality_one_relationships.py +2 -2
  19. infrahub/core/migrations/graph/m028_delete_diffs.py +1 -2
  20. infrahub/core/migrations/graph/m029_duplicates_cleanup.py +2 -2
  21. infrahub/core/migrations/graph/m031_check_number_attributes.py +102 -0
  22. infrahub/core/migrations/query/attribute_rename.py +1 -1
  23. infrahub/core/node/__init__.py +70 -37
  24. infrahub/core/path.py +14 -0
  25. infrahub/core/query/delete.py +3 -3
  26. infrahub/core/relationship/constraints/count.py +10 -9
  27. infrahub/core/relationship/constraints/interface.py +2 -1
  28. infrahub/core/relationship/constraints/peer_kind.py +2 -1
  29. infrahub/core/relationship/constraints/peer_parent.py +56 -0
  30. infrahub/core/relationship/constraints/peer_relatives.py +1 -1
  31. infrahub/core/relationship/constraints/profiles_kind.py +1 -1
  32. infrahub/core/schema/attribute_parameters.py +12 -5
  33. infrahub/core/schema/basenode_schema.py +107 -1
  34. infrahub/core/schema/definitions/internal.py +8 -1
  35. infrahub/core/schema/generated/relationship_schema.py +6 -1
  36. infrahub/core/schema/schema_branch.py +53 -13
  37. infrahub/core/validators/__init__.py +2 -1
  38. infrahub/core/validators/attribute/min_max.py +7 -2
  39. infrahub/core/validators/relationship/peer.py +174 -4
  40. infrahub/database/__init__.py +0 -1
  41. infrahub/dependencies/builder/constraint/grouped/node_runner.py +2 -0
  42. infrahub/dependencies/builder/constraint/relationship_manager/peer_parent.py +8 -0
  43. infrahub/dependencies/builder/constraint/schema/aggregated.py +2 -0
  44. infrahub/dependencies/builder/constraint/schema/relationship_peer.py +8 -0
  45. infrahub/dependencies/registry.py +2 -0
  46. infrahub/git/tasks.py +1 -0
  47. infrahub/graphql/app.py +5 -1
  48. infrahub/graphql/mutations/convert_object_type.py +16 -7
  49. infrahub/graphql/mutations/relationship.py +32 -0
  50. infrahub/graphql/queries/convert_object_type_mapping.py +3 -5
  51. infrahub/message_bus/operations/refresh/registry.py +3 -6
  52. infrahub/pools/models.py +14 -0
  53. infrahub/pools/tasks.py +71 -1
  54. infrahub/services/adapters/message_bus/nats.py +5 -1
  55. infrahub/services/scheduler.py +5 -1
  56. infrahub_sdk/ctl/generator.py +4 -4
  57. infrahub_sdk/ctl/repository.py +1 -1
  58. infrahub_sdk/node/__init__.py +2 -0
  59. infrahub_sdk/node/node.py +166 -93
  60. infrahub_sdk/pytest_plugin/items/python_transform.py +2 -1
  61. infrahub_sdk/query_groups.py +4 -3
  62. infrahub_sdk/utils.py +7 -20
  63. infrahub_sdk/yaml.py +6 -5
  64. {infrahub_server-1.3.0b5.dist-info → infrahub_server-1.3.1.dist-info}/METADATA +2 -2
  65. {infrahub_server-1.3.0b5.dist-info → infrahub_server-1.3.1.dist-info}/RECORD +68 -63
  66. {infrahub_server-1.3.0b5.dist-info → infrahub_server-1.3.1.dist-info}/LICENSE.txt +0 -0
  67. {infrahub_server-1.3.0b5.dist-info → infrahub_server-1.3.1.dist-info}/WHEEL +0 -0
  68. {infrahub_server-1.3.0b5.dist-info → infrahub_server-1.3.1.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from enum import Enum
4
+ from typing import Self
4
5
 
5
6
  from infrahub.core.constants import InfrahubKind
6
7
  from infrahub.core.schema.dropdown import DropdownChoice
@@ -9,165 +10,121 @@ from infrahub.utils import InfrahubStringEnum
9
10
 
10
11
  class NodeAction(InfrahubStringEnum):
11
12
  CREATED = "created"
12
- DELETED = "deleted"
13
13
  UPDATED = "updated"
14
14
 
15
15
 
16
- class BranchScope(Enum):
16
+ class DropdownEnum(Enum):
17
+ @classmethod
18
+ def available_types(cls) -> list[DropdownChoice]:
19
+ return [cls.__members__[member].value for member in list(cls.__members__)]
20
+
21
+ @classmethod
22
+ def from_value(cls, value: str) -> Self:
23
+ for member in cls.__members__:
24
+ if value == cls.__members__[member].value.name:
25
+ return cls.__members__[member]
26
+
27
+ raise NotImplementedError(f"The defined value {value} doesn't match a value of {cls.__class__.__name__}")
28
+
29
+
30
+ class BranchScope(DropdownEnum):
17
31
  ALL_BRANCHES = DropdownChoice(
18
32
  name="all_branches",
19
33
  label="All Branches",
20
34
  description="All branches",
21
- color="#fef08a",
35
+ color="#4cd964",
22
36
  )
23
37
  DEFAULT_BRANCH = DropdownChoice(
24
38
  name="default_branch",
25
39
  label="Default Branch",
26
40
  description="Only the default branch",
27
- color="#86efac",
41
+ color="#5ac8fa",
28
42
  )
29
43
  OTHER_BRANCHES = DropdownChoice(
30
44
  name="other_branches",
31
45
  label="Other Branches",
32
46
  description="All branches except the default branch",
33
- color="#e5e7eb",
47
+ color="#ff2d55",
34
48
  )
35
49
 
36
- @classmethod
37
- def available_types(cls) -> list[DropdownChoice]:
38
- return [cls.__members__[member].value for member in list(cls.__members__)]
39
50
 
40
- @classmethod
41
- def from_value(cls, value: str) -> BranchScope:
42
- for member in cls.__members__:
43
- if value == cls.__members__[member].value.name:
44
- return cls.__members__[member]
45
-
46
- raise NotImplementedError(f"The defined value {value} doesn't match a branch scope")
47
-
48
-
49
- class MemberAction(Enum):
51
+ class MemberAction(DropdownEnum):
50
52
  ADD_MEMBER = DropdownChoice(
51
53
  name="add_member",
52
54
  label="Add member",
53
55
  description="Add impacted member to the selected group",
54
- color="#86efac",
56
+ color="#4cd964",
55
57
  )
56
58
  REMOVE_MEMBER = DropdownChoice(
57
59
  name="remove_member",
58
60
  label="Remove member",
59
61
  description="Remove impacted member from the selected group",
60
- color="#fef08a",
62
+ color="#ff2d55",
61
63
  )
62
64
 
63
- @classmethod
64
- def available_types(cls) -> list[DropdownChoice]:
65
- return [cls.__members__[member].value for member in list(cls.__members__)]
66
-
67
- @classmethod
68
- def from_value(cls, value: str) -> MemberAction:
69
- for member in cls.__members__:
70
- if value == cls.__members__[member].value.name:
71
- return cls.__members__[member]
72
-
73
- raise NotImplementedError(f"The defined value {value} doesn't match a member action")
74
-
75
65
 
76
- class MemberUpdate(Enum):
66
+ class MemberUpdate(DropdownEnum):
77
67
  ADDED = DropdownChoice(
78
68
  name="added",
79
69
  label="Added",
80
70
  description="Trigger when members are added to this group",
81
- color="#86efac",
71
+ color="#4cd964",
82
72
  )
83
73
  REMOVED = DropdownChoice(
84
74
  name="removed",
85
75
  label="Removed",
86
76
  description="Trigger when members are removed from this group",
87
- color="#fef08a",
77
+ color="#ff2d55",
88
78
  )
89
79
 
90
- @classmethod
91
- def available_types(cls) -> list[DropdownChoice]:
92
- return [cls.__members__[member].value for member in list(cls.__members__)]
93
-
94
- @classmethod
95
- def from_value(cls, value: str) -> MemberUpdate:
96
- for member in cls.__members__:
97
- if value == cls.__members__[member].value.name:
98
- return cls.__members__[member]
99
80
 
100
- raise NotImplementedError(f"The defined value {value} doesn't match a MemberUpdate")
101
-
102
-
103
- class RelationshipMatch(Enum):
81
+ class RelationshipMatch(DropdownEnum):
104
82
  ADDED = DropdownChoice(
105
83
  name="added",
106
84
  label="Added",
107
85
  description="Check if the selected relationship was added",
108
- color="#86efac",
86
+ color="#4cd964",
109
87
  )
110
88
  REMOVED = DropdownChoice(
111
89
  name="removed",
112
90
  label="Removed",
113
91
  description="Check if the selected relationship was removed",
114
- color="#fef08a",
92
+ color="#ff2d55",
115
93
  )
116
94
  UPDATED = DropdownChoice(
117
95
  name="updated",
118
96
  label="Updated",
119
97
  description="Check if the selected relationship was updated, added or removed.",
120
- color="#e5e7eb",
98
+ color="#5ac8fa",
121
99
  )
122
100
 
123
- @classmethod
124
- def available_types(cls) -> list[DropdownChoice]:
125
- return [cls.__members__[member].value for member in list(cls.__members__)]
126
-
127
- @classmethod
128
- def from_value(cls, value: str) -> RelationshipMatch:
129
- for member in cls.__members__:
130
- if value == cls.__members__[member].value.name:
131
- return cls.__members__[member]
132
-
133
- raise NotImplementedError(f"The defined value {value} doesn't match a RelationshipMatch")
134
101
 
135
-
136
- class ValueMatch(Enum):
102
+ class ValueMatch(DropdownEnum):
137
103
  VALUE = DropdownChoice(
138
104
  name="value",
139
105
  label="Value",
140
106
  description="Match against the current value",
141
- color="#fef08a",
107
+ color="#4cd964",
142
108
  )
143
109
  VALUE_PREVIOUS = DropdownChoice(
144
110
  name="value_previous",
145
111
  label="Value Previous",
146
112
  description="Match against the previous value",
147
- color="#86efac",
113
+ color="#ff2d55",
148
114
  )
149
115
  VALUE_FULL = DropdownChoice(
150
116
  name="value_full",
151
117
  label="Full value match",
152
118
  description="Match against both the current and previous values",
153
- color="#e5e7eb",
119
+ color="#5ac8fa",
154
120
  )
155
121
 
156
- @classmethod
157
- def available_types(cls) -> list[DropdownChoice]:
158
- return [cls.__members__[member].value for member in list(cls.__members__)]
159
-
160
- @classmethod
161
- def from_value(cls, value: str) -> ValueMatch:
162
- for member in cls.__members__:
163
- if value == cls.__members__[member].value.name:
164
- return cls.__members__[member]
165
-
166
- raise NotImplementedError(f"The defined value {value} doesn't match a ValueMatch")
167
-
168
122
 
169
123
  NODES_THAT_TRIGGER_ACTION_RULES_SETUP = [
124
+ InfrahubKind.GENERATORACTION,
170
125
  InfrahubKind.GROUPACTION,
171
126
  InfrahubKind.GROUPTRIGGERRULE,
172
127
  InfrahubKind.NODETRIGGERRULE,
128
+ InfrahubKind.NODETRIGGERATTRIBUTEMATCH,
129
+ InfrahubKind.NODETRIGGERRELATIONSHIPMATCH,
173
130
  ]
@@ -271,6 +271,7 @@ core_node_trigger_attribute_match = NodeSchema(
271
271
  branch=BranchSupportType.AGNOSTIC,
272
272
  generate_profile=False,
273
273
  inherit_from=["CoreNodeTriggerMatch"],
274
+ display_labels=["attribute_name__value"],
274
275
  attributes=[
275
276
  Attr(
276
277
  name="attribute_name",
@@ -319,6 +320,7 @@ core_node_trigger_relationship_match = NodeSchema(
319
320
  branch=BranchSupportType.AGNOSTIC,
320
321
  generate_profile=False,
321
322
  inherit_from=["CoreNodeTriggerMatch"],
323
+ display_labels=["relationship_name__value", "modification_type__value"],
322
324
  attributes=[
323
325
  Attr(
324
326
  name="relationship_name",
infrahub/cli/db.py CHANGED
@@ -287,10 +287,10 @@ async def index(
287
287
  await dbdriver.close()
288
288
 
289
289
 
290
- async def migrate_database(db: InfrahubDatabase, initialize: bool = False, check: bool = False) -> None:
290
+ async def migrate_database(db: InfrahubDatabase, initialize: bool = False, check: bool = False) -> bool:
291
291
  """Apply the latest migrations to the database, this function will print the status directly in the console.
292
292
 
293
- This function is expected to run on an empty
293
+ Returns a boolean indicating whether a migration failed or if all migrations succeeded.
294
294
 
295
295
  Args:
296
296
  db: The database object.
@@ -306,14 +306,14 @@ async def migrate_database(db: InfrahubDatabase, initialize: bool = False, check
306
306
 
307
307
  if not migrations:
308
308
  rprint(f"Database up-to-date (v{root_node.graph_version}), no migration to execute.")
309
- return
309
+ return True
310
310
 
311
311
  rprint(
312
312
  f"Database needs to be updated (v{root_node.graph_version} -> v{GRAPH_VERSION}), {len(migrations)} migrations pending"
313
313
  )
314
314
 
315
315
  if check:
316
- return
316
+ return True
317
317
 
318
318
  for migration in migrations:
319
319
  execution_result = await migration.execute(db=db)
@@ -333,7 +333,9 @@ async def migrate_database(db: InfrahubDatabase, initialize: bool = False, check
333
333
  if validation_result and not validation_result.success:
334
334
  for error in validation_result.errors:
335
335
  rprint(f" {error}")
336
- break
336
+ return False
337
+
338
+ return True
337
339
 
338
340
 
339
341
  async def initialize_internal_schema() -> None:
infrahub/cli/upgrade.py CHANGED
@@ -75,7 +75,12 @@ async def upgrade_cmd(
75
75
  # -------------------------------------------
76
76
  # Upgrade Infrahub Database and Schema
77
77
  # -------------------------------------------
78
- await migrate_database(db=dbdriver, initialize=False, check=check)
78
+
79
+ if not await migrate_database(db=dbdriver, initialize=False, check=check):
80
+ # A migration failed, stop the upgrade process
81
+ rprint("Upgrade cancelled due to migration failure.")
82
+ await dbdriver.close()
83
+ return
79
84
 
80
85
  await initialize_internal_schema()
81
86
  await update_core_schema(db=dbdriver, service=service, initialize=False)
@@ -31,6 +31,7 @@ from infrahub.helpers import hash_password
31
31
 
32
32
  from ..types import ATTRIBUTE_TYPES, LARGE_ATTRIBUTE_TYPES
33
33
  from .constants.relationship_label import RELATIONSHIP_TO_NODE_LABEL, RELATIONSHIP_TO_VALUE_LABEL
34
+ from .schema.attribute_parameters import NumberAttributeParameters
34
35
 
35
36
  if TYPE_CHECKING:
36
37
  from infrahub.core.branch import Branch
@@ -255,6 +256,10 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
255
256
  if len(value) > max_length:
256
257
  raise ValidationError({name: f"{value} must have a maximum length of {max_length!r}"})
257
258
 
259
+ # Some invalid values may exist due to https://github.com/opsmill/infrahub/issues/6714.
260
+ if config.SETTINGS.main.schema_strict_mode and isinstance(schema.parameters, NumberAttributeParameters):
261
+ schema.parameters.check_valid_value(value=value, name=name)
262
+
258
263
  if schema.enum:
259
264
  try:
260
265
  schema.convert_value_to_enum(value)
@@ -38,4 +38,6 @@ class NodeConstraintRunner:
38
38
  relationship_manager: RelationshipManager = getattr(node, relationship_name)
39
39
  await relationship_manager.fetch_relationship_ids(db=db, force_refresh=True)
40
40
  for relationship_constraint in self.relationship_manager_constraints:
41
- await relationship_constraint.check(relm=relationship_manager, node_schema=node.get_schema())
41
+ await relationship_constraint.check(
42
+ relm=relationship_manager, node_schema=node.get_schema(), node=node
43
+ )
@@ -101,6 +101,8 @@ async def convert_object_type(
101
101
  deleted_node_out_rels_peer_ids = await get_out_rels_peers_ids(node=node, db=dbt)
102
102
  deleted_node_unidir_rels_peer_ids = await get_unidirectional_rels_peers_ids(node=node, db=dbt, branch=branch)
103
103
 
104
+ # Delete the node, so we delete relationships with peers as well, which might temporarily break cardinality constraints
105
+ # but they should be restored when creating the new node.
104
106
  deleted_nodes = await NodeManager.delete(db=dbt, branch=branch, nodes=[node], cascade_delete=False)
105
107
  if len(deleted_nodes) != 1:
106
108
  raise ValueError(f"Deleted {len(deleted_nodes)} nodes instead of 1")
@@ -4,7 +4,10 @@ from dataclasses import dataclass, field
4
4
  from typing import TYPE_CHECKING, Iterable, Literal, Sequence, overload
5
5
  from uuid import uuid4
6
6
 
7
+ from prefect import flow
8
+
7
9
  from infrahub import lock
10
+ from infrahub.core.branch import Branch # noqa: TC001
8
11
  from infrahub.core.timestamp import Timestamp
9
12
  from infrahub.exceptions import ValidationError
10
13
  from infrahub.log import get_logger
@@ -22,7 +25,6 @@ from .model.path import (
22
25
  )
23
26
 
24
27
  if TYPE_CHECKING:
25
- from infrahub.core.branch import Branch
26
28
  from infrahub.core.node import Node
27
29
 
28
30
  from .calculator import DiffCalculator
@@ -301,6 +303,11 @@ class DiffCoordinator:
301
303
  force_branch_refresh: Literal[False] = ...,
302
304
  ) -> tuple[EnrichedDiffs | EnrichedDiffsMetadata, set[NodeIdentifier]]: ...
303
305
 
306
+ @flow( # type: ignore[misc]
307
+ name="update-diff",
308
+ flow_run_name="Update diff for {base_branch.name} - {diff_branch.name}: ({from_time}-{to_time}),tracking_id={tracking_id}",
309
+ validate_parameters=False,
310
+ )
304
311
  async def _update_diffs(
305
312
  self,
306
313
  base_branch: Branch,
@@ -9,17 +9,21 @@ class EnrichedDiffDeleteQuery(Query):
9
9
  type = QueryType.WRITE
10
10
  insert_return = False
11
11
 
12
- def __init__(self, enriched_diff_root_uuids: list[str], **kwargs: Any) -> None:
12
+ def __init__(self, enriched_diff_root_uuids: list[str] | None = None, **kwargs: Any) -> None:
13
13
  super().__init__(**kwargs)
14
14
  self.enriched_diff_root_uuids = enriched_diff_root_uuids
15
15
 
16
16
  async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
17
- self.params = {"diff_root_uuids": self.enriched_diff_root_uuids}
17
+ diff_filter = ""
18
+ if self.enriched_diff_root_uuids:
19
+ self.params = {"diff_root_uuids": self.enriched_diff_root_uuids}
20
+ diff_filter = "WHERE d_root.uuid IN $diff_root_uuids"
21
+
18
22
  query = """
19
23
  MATCH (d_root:DiffRoot)
20
- WHERE d_root.uuid IN $diff_root_uuids
24
+ %(diff_filter)s
21
25
  OPTIONAL MATCH (d_root)-[*]->(diff_thing)
22
26
  DETACH DELETE diff_thing
23
27
  DETACH DELETE d_root
24
- """
28
+ """ % {"diff_filter": diff_filter}
25
29
  self.add_to_query(query=query)
@@ -15,7 +15,7 @@ class EnrichedDiffFieldSpecifiersQuery(Query):
15
15
  async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
16
16
  self.params["diff_id"] = self.diff_id
17
17
  query = """
18
- CALL {
18
+ CALL () {
19
19
  MATCH (root:DiffRoot {uuid: $diff_id})-[:DIFF_HAS_NODE]->(node:DiffNode)-[:DIFF_HAS_ATTRIBUTE]->(attr:DiffAttribute)
20
20
  WHERE (root.is_merged IS NULL OR root.is_merged <> TRUE)
21
21
  RETURN node.uuid AS node_uuid, node.kind AS node_kind, attr.name AS field_name
@@ -710,14 +710,14 @@ class DiffMergeRollbackQuery(Query):
710
710
  // ---------------------------
711
711
  // reset to times on target branch
712
712
  // ---------------------------
713
- CALL {
713
+ CALL () {
714
714
  OPTIONAL MATCH ()-[r_to {to: $at, branch: $target_branch}]-()
715
715
  SET r_to.to = NULL
716
716
  }
717
717
  // ---------------------------
718
718
  // reset from times on target branch
719
719
  // ---------------------------
720
- CALL {
720
+ CALL () {
721
721
  OPTIONAL MATCH ()-[r_from {from: $at, branch: $target_branch}]-()
722
722
  DELETE r_from
723
723
  }
@@ -377,6 +377,10 @@ class DiffRepository:
377
377
  await query.execute(db=self.db)
378
378
  return query.get_summary()
379
379
 
380
+ async def delete_all_diff_roots(self) -> None:
381
+ query = await EnrichedDiffDeleteQuery.init(db=self.db)
382
+ await query.execute(db=self.db)
383
+
380
384
  async def delete_diff_roots(self, diff_root_uuids: list[str]) -> None:
381
385
  query = await EnrichedDiffDeleteQuery.init(db=self.db, enriched_diff_root_uuids=diff_root_uuids)
382
386
  await query.execute(db=self.db)
@@ -1 +1 @@
1
- GRAPH_VERSION = 30
1
+ GRAPH_VERSION = 31
@@ -32,6 +32,7 @@ from .m027_delete_isolated_nodes import Migration027
32
32
  from .m028_delete_diffs import Migration028
33
33
  from .m029_duplicates_cleanup import Migration029
34
34
  from .m030_illegal_edges import Migration030
35
+ from .m031_check_number_attributes import Migration031
35
36
 
36
37
  if TYPE_CHECKING:
37
38
  from infrahub.core.root import Root
@@ -69,6 +70,7 @@ MIGRATIONS: list[type[GraphMigration | InternalSchemaMigration | ArbitraryMigrat
69
70
  Migration028,
70
71
  Migration029,
71
72
  Migration030,
73
+ Migration031,
72
74
  ]
73
75
 
74
76
 
@@ -61,7 +61,7 @@ class Migration012RenameTypeAttributeData(AttributeRenameQuery):
61
61
  def render_match(self) -> str:
62
62
  query = """
63
63
  // Find all the active nodes
64
- CALL {
64
+ CALL () {
65
65
  MATCH (node:%(node_kind)s)
66
66
  WHERE exists((node)-[:HAS_ATTRIBUTE]-(:Attribute { name: $prev_attr.name }))
67
67
  AND NOT exists((node)-[:HAS_ATTRIBUTE]-(:Attribute { name: $new_attr.name }))
@@ -31,6 +31,5 @@ class Migration015(ArbitraryMigration):
31
31
  component_registry = get_component_registry()
32
32
  diff_repo = await component_registry.get_component(DiffRepository, db=db, branch=default_branch)
33
33
 
34
- diff_roots = await diff_repo.get_roots_metadata()
35
- await diff_repo.delete_diff_roots(diff_root_uuids=[d.uuid for d in diff_roots])
34
+ await diff_repo.delete_all_diff_roots()
36
35
  return MigrationResult()
@@ -31,6 +31,5 @@ class Migration016(ArbitraryMigration):
31
31
  component_registry = get_component_registry()
32
32
  diff_repo = await component_registry.get_component(DiffRepository, db=db, branch=default_branch)
33
33
 
34
- diff_roots = await diff_repo.get_roots_metadata()
35
- await diff_repo.delete_diff_roots(diff_root_uuids=[d.uuid for d in diff_roots])
34
+ await diff_repo.delete_all_diff_roots()
36
35
  return MigrationResult()
@@ -52,7 +52,7 @@ class DedupCardinalityOneRelsQuery(Query):
52
52
  # of a one-to-many BIDIR relationship.
53
53
  query = """
54
54
 
55
- CALL {
55
+ CALL () {
56
56
  MATCH (rel_node: Relationship)-[edge:IS_RELATED]->(n: Node)<-[edge_2:IS_RELATED]-(rel_node_2: Relationship {name: rel_node.name})
57
57
  WHERE rel_node.name in $rel_one_identifiers_inbound[n.kind]
58
58
  AND edge.branch = edge_2.branch
@@ -64,7 +64,7 @@ class DedupCardinalityOneRelsQuery(Query):
64
64
  DETACH DELETE rel_node
65
65
  }
66
66
 
67
- CALL {
67
+ CALL () {
68
68
  MATCH (rel_node_3: Relationship)<-[edge_3:IS_RELATED]-(n: Node)-[edge_4:IS_RELATED]->(rel_node_4: Relationship {name: rel_node_3.name})
69
69
  WHERE rel_node_3.name in $rel_one_identifiers_outbound[n.kind]
70
70
  AND edge_3.branch = edge_4.branch
@@ -33,6 +33,5 @@ class Migration028(ArbitraryMigration):
33
33
  component_registry = get_component_registry()
34
34
  diff_repo = await component_registry.get_component(DiffRepository, db=db, branch=default_branch)
35
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])
36
+ await diff_repo.delete_all_diff_roots()
38
37
  return MigrationResult()
@@ -535,12 +535,12 @@ class PerformHardDeletes(Query):
535
535
 
536
536
  async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
537
537
  query = """
538
- CALL {
538
+ CALL () {
539
539
  MATCH (n)
540
540
  WHERE n.to_delete = TRUE
541
541
  DETACH DELETE n
542
542
  }
543
- CALL {
543
+ CALL () {
544
544
  MATCH ()-[e]-()
545
545
  WHERE e.to_delete = TRUE
546
546
  DELETE e
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Sequence
4
+
5
+ from infrahub import config
6
+ from infrahub.core import registry
7
+ from infrahub.core.branch import Branch
8
+ from infrahub.core.constants import SchemaPathType
9
+ from infrahub.core.initialization import initialization
10
+ from infrahub.core.migrations.shared import InternalSchemaMigration, MigrationResult, SchemaMigration
11
+ from infrahub.core.path import SchemaPath
12
+ from infrahub.core.schema import GenericSchema, NodeSchema
13
+ from infrahub.core.schema.attribute_parameters import NumberAttributeParameters
14
+ from infrahub.core.validators.attribute.min_max import AttributeNumberChecker
15
+ from infrahub.core.validators.enum import ConstraintIdentifier
16
+ from infrahub.core.validators.model import SchemaConstraintValidatorRequest
17
+ from infrahub.lock import initialize_lock
18
+ from infrahub.log import get_logger
19
+ from infrahub.types import Number
20
+
21
+ if TYPE_CHECKING:
22
+ from infrahub.database import InfrahubDatabase
23
+
24
+ log = get_logger()
25
+
26
+
27
+ class Migration031(InternalSchemaMigration):
28
+ """
29
+ Some nodes with invalid number attributes may have been created as min/max/excluded_values were not working properly.
30
+ This migration indicates corrupted nodes. If strict mode is disabled, both this migration and min/max/excludes_values constraints are disabled,
31
+ so that users can carry one with their corrupted data without any failure.
32
+ """
33
+
34
+ name: str = "031_check_number_attributes"
35
+ minimum_version: int = 30
36
+ migrations: Sequence[SchemaMigration] = []
37
+
38
+ async def execute(self, db: InfrahubDatabase) -> MigrationResult:
39
+ """Retrieve all number attributes that have a min/max/excluded_values
40
+ For any of these attributes, check if corresponding existing nodes are valid."""
41
+
42
+ if not config.SETTINGS.main.schema_strict_mode:
43
+ return MigrationResult()
44
+
45
+ # load schemas from database into registry
46
+ initialize_lock()
47
+ await initialization(db=db)
48
+
49
+ node_id_to_error_message = {}
50
+
51
+ branches = await Branch.get_list(db=db)
52
+ for branch in branches: # noqa
53
+ schema_branch = await registry.schema.load_schema_from_db(db=db, branch=branch)
54
+ for node_schema_kind in schema_branch.node_names:
55
+ schema = schema_branch.get_node(name=node_schema_kind, duplicate=False)
56
+ if not isinstance(schema, (NodeSchema, GenericSchema)):
57
+ continue
58
+
59
+ for attr in schema.attributes:
60
+ if attr.kind != Number.label:
61
+ continue
62
+
63
+ # Check if the attribute has a min/max/excluded_values being violated
64
+ if isinstance(attr.parameters, NumberAttributeParameters) and (
65
+ attr.parameters.min_value is not None
66
+ or attr.parameters.max_value is not None
67
+ or attr.parameters.excluded_values
68
+ ):
69
+ request = SchemaConstraintValidatorRequest(
70
+ branch=branch,
71
+ constraint_name=ConstraintIdentifier.ATTRIBUTE_PARAMETERS_MIN_VALUE_UPDATE.value,
72
+ node_schema=schema,
73
+ schema_path=SchemaPath(
74
+ path_type=SchemaPathType.ATTRIBUTE, schema_kind=schema.kind, field_name=attr.name
75
+ ),
76
+ schema_branch=db.schema.get_schema_branch(name=registry.default_branch),
77
+ )
78
+
79
+ constraint_checker = AttributeNumberChecker(db=db, branch=branch)
80
+ grouped_data_paths = await constraint_checker.check(request)
81
+ data_paths = grouped_data_paths[0].get_all_data_paths()
82
+ for data_path in data_paths:
83
+ # Avoid having duplicated error messages for nodes present on multiple branches.
84
+ if data_path.node_id not in node_id_to_error_message:
85
+ node_id_to_error_message[data_path.node_id] = (
86
+ f"Node {data_path.node_id} on branch {branch.name} "
87
+ f"has an invalid Number attribute {data_path.field_name}: {data_path.value}"
88
+ )
89
+
90
+ if len(node_id_to_error_message) == 0:
91
+ return MigrationResult()
92
+
93
+ error_str = (
94
+ "Following nodes attributes values must be updated to not violate corresponding min_value, "
95
+ "max_value or excluded_values schema constraints"
96
+ )
97
+ errors_messages = list(node_id_to_error_message.values())
98
+ return MigrationResult(errors=[error_str] + errors_messages)
99
+
100
+ async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
101
+ result = MigrationResult()
102
+ return result
@@ -38,7 +38,7 @@ class AttributeRenameQuery(Query):
38
38
  def render_match(self) -> str:
39
39
  query = """
40
40
  // Find all the active nodes
41
- CALL {
41
+ CALL () {
42
42
  MATCH (node:%(node_kind)s)
43
43
  WHERE exists((node)-[:HAS_ATTRIBUTE]-(:Attribute { name: $prev_attr.name }))
44
44
  RETURN node