infrahub-server 1.7.0rc0__py3-none-any.whl → 1.7.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 (80) hide show
  1. infrahub/api/schema.py +5 -0
  2. infrahub/cli/db.py +6 -2
  3. infrahub/core/branch/models.py +11 -117
  4. infrahub/core/branch/tasks.py +7 -3
  5. infrahub/core/diff/merger/merger.py +5 -1
  6. infrahub/core/graph/__init__.py +1 -1
  7. infrahub/core/initialization.py +2 -1
  8. infrahub/core/migrations/graph/__init__.py +2 -0
  9. infrahub/core/migrations/graph/m014_remove_index_attr_value.py +3 -2
  10. infrahub/core/migrations/graph/m015_diff_format_update.py +3 -2
  11. infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +3 -2
  12. infrahub/core/migrations/graph/m017_add_core_profile.py +6 -4
  13. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +3 -4
  14. infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -3
  15. infrahub/core/migrations/graph/m025_uniqueness_nulls.py +3 -4
  16. infrahub/core/migrations/graph/m026_0000_prefix_fix.py +4 -5
  17. infrahub/core/migrations/graph/m028_delete_diffs.py +3 -2
  18. infrahub/core/migrations/graph/m029_duplicates_cleanup.py +3 -2
  19. infrahub/core/migrations/graph/m031_check_number_attributes.py +4 -3
  20. infrahub/core/migrations/graph/m032_cleanup_orphaned_branch_relationships.py +3 -2
  21. infrahub/core/migrations/graph/m034_find_orphaned_schema_fields.py +3 -2
  22. infrahub/core/migrations/graph/m035_orphan_relationships.py +3 -3
  23. infrahub/core/migrations/graph/m036_drop_attr_value_index.py +3 -2
  24. infrahub/core/migrations/graph/m037_index_attr_vals.py +3 -2
  25. infrahub/core/migrations/graph/m038_redo_0000_prefix_fix.py +4 -5
  26. infrahub/core/migrations/graph/m039_ipam_reconcile.py +3 -2
  27. infrahub/core/migrations/graph/m041_deleted_dup_edges.py +3 -2
  28. infrahub/core/migrations/graph/m042_profile_attrs_in_db.py +5 -4
  29. infrahub/core/migrations/graph/m043_create_hfid_display_label_in_db.py +12 -5
  30. infrahub/core/migrations/graph/m044_backfill_hfid_display_label_in_db.py +15 -4
  31. infrahub/core/migrations/graph/m045_backfill_hfid_display_label_in_db_profile_template.py +10 -4
  32. infrahub/core/migrations/graph/m046_fill_agnostic_hfid_display_labels.py +6 -5
  33. infrahub/core/migrations/graph/m047_backfill_or_null_display_label.py +19 -5
  34. infrahub/core/migrations/graph/m048_undelete_rel_props.py +6 -4
  35. infrahub/core/migrations/graph/m049_remove_is_visible_relationship.py +3 -3
  36. infrahub/core/migrations/graph/m050_backfill_vertex_metadata.py +3 -3
  37. infrahub/core/migrations/graph/m051_subtract_branched_from_microsecond.py +39 -0
  38. infrahub/core/migrations/runner.py +6 -3
  39. infrahub/core/migrations/schema/attribute_kind_update.py +8 -11
  40. infrahub/core/migrations/schema/attribute_supports_profile.py +3 -8
  41. infrahub/core/migrations/schema/models.py +8 -0
  42. infrahub/core/migrations/schema/node_attribute_add.py +10 -13
  43. infrahub/core/migrations/schema/tasks.py +7 -1
  44. infrahub/core/migrations/shared.py +37 -30
  45. infrahub/core/node/__init__.py +2 -1
  46. infrahub/core/relationship/model.py +8 -2
  47. infrahub/core/schema/attribute_parameters.py +28 -1
  48. infrahub/core/schema/attribute_schema.py +9 -2
  49. infrahub/core/schema/manager.py +50 -38
  50. infrahub/core/validators/attribute/kind.py +5 -2
  51. infrahub/graphql/manager.py +8 -2
  52. infrahub/lock.py +7 -0
  53. infrahub/services/adapters/cache/redis.py +7 -0
  54. infrahub_sdk/analyzer.py +2 -2
  55. infrahub_sdk/branch.py +12 -39
  56. infrahub_sdk/checks.py +4 -4
  57. infrahub_sdk/client.py +36 -0
  58. infrahub_sdk/ctl/cli_commands.py +2 -1
  59. infrahub_sdk/ctl/graphql.py +15 -4
  60. infrahub_sdk/ctl/utils.py +2 -2
  61. infrahub_sdk/enums.py +6 -0
  62. infrahub_sdk/graphql/renderers.py +21 -0
  63. infrahub_sdk/graphql/utils.py +85 -0
  64. infrahub_sdk/node/attribute.py +12 -2
  65. infrahub_sdk/node/constants.py +11 -0
  66. infrahub_sdk/node/metadata.py +69 -0
  67. infrahub_sdk/node/node.py +65 -14
  68. infrahub_sdk/node/property.py +3 -0
  69. infrahub_sdk/node/related_node.py +24 -1
  70. infrahub_sdk/node/relationship.py +10 -1
  71. infrahub_sdk/operation.py +2 -2
  72. infrahub_sdk/schema/repository.py +1 -2
  73. infrahub_sdk/transforms.py +2 -2
  74. infrahub_sdk/types.py +18 -2
  75. {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.1.dist-info}/METADATA +6 -6
  76. {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.1.dist-info}/RECORD +80 -77
  77. {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.1.dist-info}/entry_points.txt +0 -1
  78. infrahub_testcontainers/performance_test.py +1 -1
  79. {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.1.dist-info}/WHEEL +0 -0
  80. {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.1.dist-info}/licenses/LICENSE.txt +0 -0
@@ -2,15 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any, Sequence
4
4
 
5
- from infrahub.core.constants import SYSTEM_USER_ID
6
5
  from infrahub.types import is_large_attribute_type
7
6
 
8
7
  from ..query import AttributeMigrationQuery, MigrationBaseQuery
9
- from ..shared import AttributeSchemaMigration, MigrationResult
8
+ from ..shared import AttributeSchemaMigration, MigrationInput, MigrationResult
10
9
 
11
10
  if TYPE_CHECKING:
12
11
  from infrahub.core.branch.models import Branch
13
- from infrahub.core.timestamp import Timestamp
14
12
  from infrahub.database import InfrahubDatabase
15
13
 
16
14
 
@@ -40,7 +38,7 @@ class AttributeKindUpdateMigrationQuery(AttributeMigrationQuery):
40
38
  // ------------
41
39
  // start with all the Attribute vertices we might care about
42
40
  // ------------
43
- MATCH (n:%(schema_kind)s)-[:HAS_ATTRIBUTE]->(attr:Attribute)
41
+ MATCH (n:%(schema_kinds)s)-[:HAS_ATTRIBUTE]->(attr:Attribute)
44
42
  WHERE attr.name = $attr_name
45
43
  WITH DISTINCT n, attr
46
44
 
@@ -76,7 +74,7 @@ CALL (av_is_default, av_value) {
76
74
  // ------------
77
75
  WITH 1 AS one
78
76
  LIMIT 1
79
- MATCH (n:%(schema_kind)s)-[:HAS_ATTRIBUTE]->(attr:Attribute)
77
+ MATCH (n:%(schema_kinds)s)-[:HAS_ATTRIBUTE]->(attr:Attribute)
80
78
  WHERE attr.name = $attr_name
81
79
  WITH DISTINCT n, attr
82
80
 
@@ -94,7 +92,6 @@ CALL (n, attr) {
94
92
  RETURN has_value_e, av
95
93
  }
96
94
 
97
-
98
95
  // ------------
99
96
  // create and update the HAS_VALUE edges
100
97
  // ------------
@@ -154,7 +151,9 @@ CALL (attr, n) {
154
151
  SET n.updated_at = $at, n.updated_by = $user_id
155
152
  }
156
153
  """ % {
157
- "schema_kind": self.migration.previous_schema.kind,
154
+ "schema_kinds": (
155
+ f"{self.migration.previous_schema.kind}|Profile{self.migration.previous_schema.kind}|Template{self.migration.previous_schema.kind}"
156
+ ),
158
157
  "branch_filter": branch_filter,
159
158
  "new_attr_value_labels": new_attr_value_labels,
160
159
  }
@@ -167,15 +166,13 @@ class AttributeKindUpdateMigration(AttributeSchemaMigration):
167
166
 
168
167
  async def execute(
169
168
  self,
170
- db: InfrahubDatabase,
169
+ migration_input: MigrationInput,
171
170
  branch: Branch,
172
- at: Timestamp | str | None = None,
173
171
  queries: Sequence[type[MigrationBaseQuery]] | None = None,
174
- user_id: str = SYSTEM_USER_ID,
175
172
  ) -> MigrationResult:
176
173
  is_indexed_previous = is_large_attribute_type(self.previous_attribute_schema.kind)
177
174
  is_indexed_new = is_large_attribute_type(self.new_attribute_schema.kind)
178
175
  if is_indexed_previous is is_indexed_new:
179
176
  return MigrationResult()
180
177
 
181
- return await super().execute(db=db, branch=branch, at=at, queries=queries, user_id=user_id)
178
+ return await super().execute(migration_input=migration_input, branch=branch, queries=queries)
@@ -2,20 +2,17 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any, Sequence
4
4
 
5
- from infrahub.core.constants import SYSTEM_USER_ID
6
5
  from infrahub.core.migrations.query.attribute_remove import AttributeRemoveQuery
7
6
  from infrahub.core.schema.generic_schema import GenericSchema
8
7
  from infrahub.core.schema.node_schema import NodeSchema
9
8
 
10
9
  from ..query import AttributeMigrationQuery, MigrationBaseQuery
11
10
  from ..query.attribute_add import AttributeAddQuery
12
- from ..shared import AttributeSchemaMigration, MigrationResult
11
+ from ..shared import AttributeSchemaMigration, MigrationInput, MigrationResult
13
12
 
14
13
  if TYPE_CHECKING:
15
14
  from infrahub.core.branch.models import Branch
16
15
  from infrahub.core.schema import MainSchemaTypes
17
- from infrahub.core.timestamp import Timestamp
18
- from infrahub.database import InfrahubDatabase
19
16
 
20
17
 
21
18
  def _get_node_kinds(schema: MainSchemaTypes) -> list[str]:
@@ -70,11 +67,9 @@ class AttributeSupportsProfileUpdateMigration(AttributeSchemaMigration):
70
67
 
71
68
  async def execute(
72
69
  self,
73
- db: InfrahubDatabase,
70
+ migration_input: MigrationInput,
74
71
  branch: Branch,
75
- at: Timestamp | str | None = None,
76
72
  queries: Sequence[type[MigrationBaseQuery]] | None = None, # noqa: ARG002
77
- user_id: str = SYSTEM_USER_ID,
78
73
  ) -> MigrationResult:
79
74
  if (
80
75
  # no change in whether the attribute should be used on profiles
@@ -89,4 +84,4 @@ class AttributeSupportsProfileUpdateMigration(AttributeSchemaMigration):
89
84
  if not self.new_attribute_schema.support_profiles:
90
85
  profiles_queries.append(ProfilesAttributeRemoveMigrationQuery)
91
86
 
92
- return await super().execute(db=db, branch=branch, at=at, queries=profiles_queries, user_id=user_id)
87
+ return await super().execute(migration_input=migration_input, branch=branch, queries=profiles_queries)
@@ -7,6 +7,7 @@ from infrahub.core.constants import SYSTEM_USER_ID
7
7
  from infrahub.core.models import SchemaUpdateMigrationInfo
8
8
  from infrahub.core.path import SchemaPath
9
9
  from infrahub.core.schema.schema_branch import SchemaBranch
10
+ from infrahub.core.timestamp import Timestamp
10
11
 
11
12
 
12
13
  class SchemaApplyMigrationData(BaseModel):
@@ -16,6 +17,7 @@ class SchemaApplyMigrationData(BaseModel):
16
17
  new_schema: SchemaBranch
17
18
  previous_schema: SchemaBranch
18
19
  migrations: list[SchemaUpdateMigrationInfo]
20
+ at: Timestamp
19
21
  user_id: str = SYSTEM_USER_ID
20
22
 
21
23
  @model_serializer()
@@ -26,6 +28,7 @@ class SchemaApplyMigrationData(BaseModel):
26
28
  "new_schema": self.new_schema.to_dict_schema_object(),
27
29
  "migrations": [migration.model_dump() for migration in self.migrations],
28
30
  "user_id": self.user_id,
31
+ "at": self.at.to_string(),
29
32
  }
30
33
 
31
34
  @field_validator("new_schema", "previous_schema", mode="before")
@@ -33,6 +36,11 @@ class SchemaApplyMigrationData(BaseModel):
33
36
  def validate_schema_branch(cls, value: Any) -> SchemaBranch:
34
37
  return SchemaBranch.validate(data=value)
35
38
 
39
+ @field_validator("at", mode="before")
40
+ @classmethod
41
+ def validate_at(cls, value: Any) -> Timestamp:
42
+ return Timestamp(value)
43
+
36
44
 
37
45
  class SchemaMigrationPathResponseData(BaseModel):
38
46
  errors: list[str] = Field(default_factory=list)
@@ -3,7 +3,6 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING, Any, Sequence
4
4
 
5
5
  from infrahub.core import registry
6
- from infrahub.core.constants import SYSTEM_USER_ID
7
6
  from infrahub.core.node import Node
8
7
  from infrahub.core.schema.generic_schema import GenericSchema
9
8
  from infrahub.core.schema.node_schema import NodeSchema
@@ -12,16 +11,14 @@ from infrahub.tasks.registry import update_branch_registry
12
11
 
13
12
  from ..query import AttributeMigrationQuery, MigrationBaseQuery
14
13
  from ..query.attribute_add import AttributeAddQuery
15
- from ..shared import AttributeSchemaMigration, MigrationResult
14
+ from ..shared import AttributeSchemaMigration, MigrationInput, MigrationResult
16
15
 
17
16
  if TYPE_CHECKING:
18
17
  from infrahub.core.node.resource_manager.number_pool import CoreNumberPool
19
18
  from infrahub.core.schema import MainSchemaTypes
20
19
  from infrahub.core.schema.attribute_schema import AttributeSchema
21
- from infrahub.database import InfrahubDatabase
22
20
 
23
21
  from ...branch import Branch
24
- from ...timestamp import Timestamp
25
22
 
26
23
 
27
24
  class NodeAttributeAddMigrationQuery01(AttributeMigrationQuery, AttributeAddQuery):
@@ -62,32 +59,32 @@ class NodeAttributeAddMigration(AttributeSchemaMigration):
62
59
 
63
60
  async def execute(
64
61
  self,
65
- db: InfrahubDatabase,
62
+ migration_input: MigrationInput,
66
63
  branch: Branch,
67
- at: Timestamp | str | None = None,
68
64
  queries: Sequence[type[MigrationBaseQuery]] | None = None,
69
- user_id: str = SYSTEM_USER_ID,
70
65
  ) -> MigrationResult:
71
66
  if self.new_attribute_schema.inherited is True:
72
67
  return MigrationResult()
73
- return await super().execute(db=db, branch=branch, at=at, queries=queries, user_id=user_id)
68
+ return await super().execute(migration_input=migration_input, branch=branch, queries=queries)
74
69
 
75
70
  async def execute_post_queries(
76
71
  self,
77
- db: InfrahubDatabase,
72
+ migration_input: MigrationInput,
78
73
  result: MigrationResult,
79
74
  branch: Branch,
80
- at: Timestamp, # noqa: ARG002
81
- user_id: str, # noqa: ARG002
82
75
  ) -> MigrationResult:
83
76
  if self.new_attribute_schema.kind != "NumberPool":
84
77
  return result
85
78
 
79
+ db = migration_input.db
80
+ at = migration_input.at
81
+
86
82
  number_pool: CoreNumberPool = await Node.fetch_or_create_number_pool(
87
83
  db=db,
88
84
  branch=branch,
89
85
  schema_node=self.new_schema, # type: ignore
90
86
  schema_attribute=self.new_attribute_schema,
87
+ at=at,
91
88
  )
92
89
 
93
90
  await update_branch_registry(db=db, branch=branch)
@@ -108,11 +105,11 @@ class NodeAttributeAddMigration(AttributeSchemaMigration):
108
105
  return result
109
106
 
110
107
  for node, number in zip(nodes, numbers, strict=True):
111
- await number_pool.reserve(db=db, number=number, identifier=node.get_id())
108
+ await number_pool.reserve(db=db, number=number, identifier=node.get_id(), at=at)
112
109
  attr = getattr(node, self.new_attribute_schema.name)
113
110
  attr.value = number
114
111
  attr.source = number_pool.id
115
112
 
116
- await node.save(db=db, fields=[self.new_attribute_schema.name])
113
+ await node.save(db=db, fields=[self.new_attribute_schema.name], at=at)
117
114
 
118
115
  return result
@@ -10,6 +10,7 @@ from prefect.logging import get_run_logger
10
10
  from infrahub.core.branch import Branch # noqa: TC001
11
11
  from infrahub.core.constants import SYSTEM_USER_ID
12
12
  from infrahub.core.migrations import MIGRATION_MAP
13
+ from infrahub.core.migrations.shared import MigrationInput
13
14
  from infrahub.core.path import SchemaPath # noqa: TC001
14
15
  from infrahub.workers.dependencies import get_database
15
16
  from infrahub.workflows.utils import add_branch_tag
@@ -18,6 +19,7 @@ from .models import SchemaApplyMigrationData, SchemaMigrationPathResponseData
18
19
 
19
20
  if TYPE_CHECKING:
20
21
  from infrahub.core.schema import MainSchemaTypes
22
+ from infrahub.core.timestamp import Timestamp
21
23
  from infrahub.database import InfrahubDatabase
22
24
 
23
25
 
@@ -59,6 +61,7 @@ async def schema_apply_migrations(message: SchemaApplyMigrationData) -> list[str
59
61
  schema_path=migration.path,
60
62
  database=await get_database(),
61
63
  user_id=message.user_id,
64
+ at=message.at,
62
65
  )
63
66
 
64
67
  async for _, result in batch.execute():
@@ -79,6 +82,7 @@ async def schema_path_migrate(
79
82
  migration_name: str,
80
83
  schema_path: SchemaPath,
81
84
  database: InfrahubDatabase,
85
+ at: Timestamp,
82
86
  new_node_schema: MainSchemaTypes | None = None,
83
87
  previous_node_schema: MainSchemaTypes | None = None,
84
88
  user_id: str = SYSTEM_USER_ID,
@@ -104,7 +108,9 @@ async def schema_path_migrate(
104
108
  previous_node_schema=previous_node_schema, # type: ignore[arg-type]
105
109
  schema_path=schema_path,
106
110
  )
107
- execution_result = await migration.execute(db=db, branch=branch, user_id=user_id)
111
+ execution_result = await migration.execute(
112
+ migration_input=MigrationInput(db=db, at=at, user_id=user_id), branch=branch
113
+ )
108
114
 
109
115
  log.info(f"Migration completed for {migration_name}")
110
116
  log.debug(f"execution_result {execution_result}")
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from dataclasses import dataclass, field
3
4
  from typing import TYPE_CHECKING, Any, Sequence
4
5
 
5
6
  from pydantic import BaseModel, ConfigDict, Field
@@ -46,6 +47,13 @@ class MigrationResult(BaseModel):
46
47
  return False
47
48
 
48
49
 
50
+ @dataclass
51
+ class MigrationInput:
52
+ db: InfrahubDatabase
53
+ at: Timestamp = field(default_factory=Timestamp)
54
+ user_id: str = SYSTEM_USER_ID
55
+
56
+
49
57
  class SchemaMigration(BaseModel):
50
58
  model_config = ConfigDict(arbitrary_types_allowed=True)
51
59
  name: str = Field(..., description="Name of the migration")
@@ -59,37 +67,37 @@ class SchemaMigration(BaseModel):
59
67
 
60
68
  async def execute_pre_queries(
61
69
  self,
62
- db: InfrahubDatabase, # noqa: ARG002
70
+ migration_input: MigrationInput, # noqa: ARG002
63
71
  result: MigrationResult,
64
72
  branch: Branch, # noqa: ARG002
65
- at: Timestamp, # noqa: ARG002
66
- user_id: str, # noqa: ARG002
67
73
  ) -> MigrationResult:
68
74
  return result
69
75
 
70
76
  async def execute_post_queries(
71
77
  self,
72
- db: InfrahubDatabase, # noqa: ARG002
78
+ migration_input: MigrationInput, # noqa: ARG002
73
79
  result: MigrationResult,
74
80
  branch: Branch, # noqa: ARG002
75
- at: Timestamp, # noqa: ARG002
76
- user_id: str, # noqa: ARG002
77
81
  ) -> MigrationResult:
78
82
  return result
79
83
 
80
84
  async def execute_queries(
81
85
  self,
82
- db: InfrahubDatabase,
86
+ migration_input: MigrationInput,
83
87
  result: MigrationResult,
84
88
  branch: Branch,
85
- at: Timestamp,
86
89
  queries: Sequence[type[MigrationBaseQuery]],
87
- user_id: str,
88
90
  ) -> MigrationResult:
89
91
  for migration_query in queries:
90
92
  try:
91
- query = await migration_query.init(db=db, branch=branch, at=at, migration=self, user_id=user_id)
92
- await query.execute(db=db)
93
+ query = await migration_query.init(
94
+ db=migration_input.db,
95
+ branch=branch,
96
+ at=migration_input.at,
97
+ migration=self,
98
+ user_id=migration_input.user_id,
99
+ )
100
+ await query.execute(db=migration_input.db)
93
101
  result.nbr_migrations_executed += query.get_nbr_migrations_executed()
94
102
  except Exception as exc:
95
103
  result.errors.append(str(exc))
@@ -99,22 +107,20 @@ class SchemaMigration(BaseModel):
99
107
 
100
108
  async def execute(
101
109
  self,
102
- db: InfrahubDatabase,
110
+ migration_input: MigrationInput,
103
111
  branch: Branch,
104
- at: Timestamp | str | None = None,
105
112
  queries: Sequence[type[MigrationBaseQuery]] | None = None,
106
- user_id: str = SYSTEM_USER_ID,
107
113
  ) -> MigrationResult:
108
- async with db.start_transaction() as ts:
114
+ async with migration_input.db.start_transaction() as ts:
109
115
  result = MigrationResult()
110
- at = Timestamp(at)
116
+ txn_migration_input = MigrationInput(db=ts, at=migration_input.at, user_id=migration_input.user_id)
111
117
 
112
- await self.execute_pre_queries(db=ts, result=result, branch=branch, at=at, user_id=user_id)
118
+ await self.execute_pre_queries(migration_input=txn_migration_input, result=result, branch=branch)
113
119
  queries_to_execute = queries or self.queries
114
120
  await self.execute_queries(
115
- db=ts, result=result, branch=branch, at=at, queries=queries_to_execute, user_id=user_id
121
+ migration_input=txn_migration_input, result=result, branch=branch, queries=queries_to_execute
116
122
  )
117
- await self.execute_post_queries(db=ts, result=result, branch=branch, at=at, user_id=user_id)
123
+ await self.execute_post_queries(migration_input=txn_migration_input, result=result, branch=branch)
118
124
 
119
125
  return result
120
126
 
@@ -174,16 +180,17 @@ class GraphMigration(BaseModel):
174
180
  async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult:
175
181
  raise NotImplementedError
176
182
 
177
- async def execute(self, db: InfrahubDatabase) -> MigrationResult:
178
- async with db.start_transaction() as ts:
179
- return await self.do_execute(db=ts)
183
+ async def execute(self, migration_input: MigrationInput) -> MigrationResult:
184
+ async with migration_input.db.start_transaction() as ts:
185
+ txn_migration_input = MigrationInput(db=ts, at=migration_input.at)
186
+ return await self.do_execute(migration_input=txn_migration_input)
180
187
 
181
- async def do_execute(self, db: InfrahubDatabase) -> MigrationResult:
188
+ async def do_execute(self, migration_input: MigrationInput) -> MigrationResult:
182
189
  result = MigrationResult()
183
190
  for migration_query in self.queries:
184
191
  try:
185
- query = await migration_query.init(db=db)
186
- await query.execute(db=db)
192
+ query = await migration_query.init(db=migration_input.db, at=migration_input.at)
193
+ await query.execute(db=migration_input.db)
187
194
  except Exception as exc:
188
195
  result.errors.append(str(exc))
189
196
  return result
@@ -216,14 +223,14 @@ class InternalSchemaMigration(BaseModel):
216
223
  async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult:
217
224
  raise NotImplementedError
218
225
 
219
- async def execute(self, db: InfrahubDatabase, user_id: str = SYSTEM_USER_ID) -> MigrationResult:
226
+ async def execute(self, migration_input: MigrationInput) -> MigrationResult:
220
227
  result = MigrationResult()
221
228
 
222
229
  default_branch = registry.get_branch_from_registry()
223
230
 
224
231
  for migration in self.migrations:
225
232
  try:
226
- execution_result = await migration.execute(db=db, branch=default_branch, user_id=user_id)
233
+ execution_result = await migration.execute(migration_input=migration_input, branch=default_branch)
227
234
  result.errors.extend(execution_result.errors)
228
235
  except Exception as exc:
229
236
  result.errors.append(str(exc))
@@ -243,7 +250,7 @@ class ArbitraryMigration(BaseModel):
243
250
  async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult:
244
251
  raise NotImplementedError()
245
252
 
246
- async def execute(self, db: InfrahubDatabase) -> MigrationResult:
253
+ async def execute(self, migration_input: MigrationInput) -> MigrationResult:
247
254
  raise NotImplementedError()
248
255
 
249
256
 
@@ -259,11 +266,11 @@ class MigrationRequiringRebase(BaseModel):
259
266
  async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult:
260
267
  raise NotImplementedError()
261
268
 
262
- async def execute_against_branch(self, db: InfrahubDatabase, branch: Branch) -> MigrationResult:
269
+ async def execute_against_branch(self, migration_input: MigrationInput, branch: Branch) -> MigrationResult:
263
270
  """Method that will be run against non-default branches, it assumes that the branches have been rebased."""
264
271
  raise NotImplementedError()
265
272
 
266
- async def execute(self, db: InfrahubDatabase) -> MigrationResult:
273
+ async def execute(self, migration_input: MigrationInput) -> MigrationResult:
267
274
  """Method that will be run against the default branch."""
268
275
  raise NotImplementedError()
269
276
 
@@ -408,6 +408,7 @@ class Node(BaseNode, MetadataInterface, metaclass=BaseNodeMeta):
408
408
  schema_node: NodeSchema | GenericSchema,
409
409
  schema_attribute: AttributeSchema,
410
410
  branch: Branch | None = None,
411
+ at: Timestamp | None = None,
411
412
  ) -> CoreNumberPool:
412
413
  """Fetch or create a number pool based on the schema attribute parameters.
413
414
 
@@ -456,7 +457,7 @@ class Node(BaseNode, MetadataInterface, metaclass=BaseNodeMeta):
456
457
  end_range=number_pool_parameters.end_range,
457
458
  pool_type=NumberPoolType.SCHEMA.value,
458
459
  )
459
- await number_pool.save(db=db)
460
+ await number_pool.save(db=db, at=at)
460
461
 
461
462
  # Do a lookup of the number pool to get the correct mapped type from the registry
462
463
  # without this we don't get access to the .get_resource() method.
@@ -1181,6 +1181,7 @@ class RelationshipManager:
1181
1181
  db: InfrahubDatabase,
1182
1182
  process_delete: bool = True,
1183
1183
  user_id: str = SYSTEM_USER_ID,
1184
+ at: Timestamp | None = None,
1184
1185
  ) -> bool:
1185
1186
  """Replace and Update the list of relationships with this one."""
1186
1187
  if not isinstance(data, list):
@@ -1189,6 +1190,7 @@ class RelationshipManager:
1189
1190
  list_data = data
1190
1191
 
1191
1192
  await self._validate_hierarchy()
1193
+ update_at = Timestamp(at)
1192
1194
 
1193
1195
  # Reset the list of relationship and save the previous one to see if we can reuse some
1194
1196
  previous_relationships = {rel.peer_id: rel for rel in await self.get_relationships(db=db) if rel.peer_id}
@@ -1211,7 +1213,7 @@ class RelationshipManager:
1211
1213
  if previous_relationships:
1212
1214
  if process_delete:
1213
1215
  for rel in previous_relationships.values():
1214
- await rel.delete(db=db, user_id=user_id)
1216
+ await rel.delete(db=db, at=update_at, user_id=user_id)
1215
1217
  changed = True
1216
1218
  continue
1217
1219
 
@@ -1231,7 +1233,11 @@ class RelationshipManager:
1231
1233
  # If the item is not present in the previous list of relationship, we create a new one.
1232
1234
  self._relationships.append(
1233
1235
  await self.rel_class(
1234
- schema=self.schema, branch=self.branch, source_kind=self.node.get_kind(), at=self.at, node=self.node
1236
+ schema=self.schema,
1237
+ branch=self.branch,
1238
+ source_kind=self.node.get_kind(),
1239
+ at=update_at,
1240
+ node=self.node,
1235
1241
  ).new(db=db, data=item)
1236
1242
  )
1237
1243
  changed = True
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import sys
4
- from typing import Self
4
+ from typing import Any, Self
5
5
 
6
6
  from pydantic import ConfigDict, Field, model_validator
7
7
 
@@ -24,6 +24,33 @@ def get_attribute_parameters_class_for_kind(kind: str) -> type[AttributeParamete
24
24
  class AttributeParameters(HashableModel):
25
25
  model_config = ConfigDict(extra="forbid")
26
26
 
27
+ @classmethod
28
+ def convert_from(cls, source: AttributeParameters) -> Self:
29
+ """Convert from another AttributeParameters subclass.
30
+
31
+ Args:
32
+ source: The source AttributeParameters instance to convert from
33
+
34
+ Returns:
35
+ A new instance of the target class with compatible fields populated
36
+ """
37
+ source_data = source.model_dump()
38
+ return cls.convert_from_dict(source_data=source_data)
39
+
40
+ @classmethod
41
+ def convert_from_dict(cls, source_data: dict[str, Any]) -> Self:
42
+ """Convert from a dictionary to the target class.
43
+
44
+ Args:
45
+ source_data: The source dictionary to convert from
46
+
47
+ Returns:
48
+ A new instance of the target class with compatible fields populated
49
+ """
50
+ target_fields = set(cls.model_fields.keys())
51
+ filtered_data = {k: v for k, v in source_data.items() if k in target_fields}
52
+ return cls(**filtered_data)
53
+
27
54
 
28
55
  class TextAttributeParameters(AttributeParameters):
29
56
  regex: str | None = Field(
@@ -114,13 +114,20 @@ class AttributeSchema(GeneratedAttributeSchema):
114
114
  @field_validator("parameters", mode="before")
115
115
  @classmethod
116
116
  def set_parameters_type(cls, value: Any, info: ValidationInfo) -> Any:
117
- """Override parameters class if using base AttributeParameters class and should be using a subclass"""
117
+ """Override parameters class if using base AttributeParameters class and should be using a subclass.
118
+
119
+ This validator handles parameter type conversion when an attribute's kind changes.
120
+ Fields from the source that don't exist in the target are silently dropped.
121
+ Fields with the same name in both classes are preserved.
122
+ """
118
123
  kind = info.data["kind"]
119
124
  expected_parameters_class = get_attribute_parameters_class_for_kind(kind=kind)
120
125
  if value is None:
121
126
  return expected_parameters_class()
122
127
  if not isinstance(value, expected_parameters_class) and isinstance(value, AttributeParameters):
123
- return expected_parameters_class(**value.model_dump())
128
+ return expected_parameters_class.convert_from(value)
129
+ if isinstance(value, dict):
130
+ return expected_parameters_class.convert_from_dict(source_data=value)
124
131
  return value
125
132
 
126
133
  @model_validator(mode="after")