infrahub-server 1.2.9rc0__py3-none-any.whl → 1.3.0a0__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 (166) hide show
  1. infrahub/actions/constants.py +86 -0
  2. infrahub/actions/gather.py +114 -0
  3. infrahub/actions/models.py +241 -0
  4. infrahub/actions/parsers.py +104 -0
  5. infrahub/actions/schema.py +382 -0
  6. infrahub/actions/tasks.py +126 -0
  7. infrahub/actions/triggers.py +21 -0
  8. infrahub/cli/db.py +1 -2
  9. infrahub/computed_attribute/models.py +13 -0
  10. infrahub/computed_attribute/tasks.py +48 -26
  11. infrahub/config.py +9 -0
  12. infrahub/core/account.py +24 -47
  13. infrahub/core/attribute.py +53 -14
  14. infrahub/core/branch/models.py +8 -9
  15. infrahub/core/branch/tasks.py +0 -2
  16. infrahub/core/constants/infrahubkind.py +8 -0
  17. infrahub/core/constraint/node/runner.py +1 -1
  18. infrahub/core/convert_object_type/__init__.py +0 -0
  19. infrahub/core/convert_object_type/conversion.py +122 -0
  20. infrahub/core/convert_object_type/schema_mapping.py +56 -0
  21. infrahub/core/diff/calculator.py +65 -11
  22. infrahub/core/diff/combiner.py +38 -31
  23. infrahub/core/diff/coordinator.py +44 -28
  24. infrahub/core/diff/data_check_synchronizer.py +3 -2
  25. infrahub/core/diff/enricher/hierarchy.py +36 -27
  26. infrahub/core/diff/ipam_diff_parser.py +5 -4
  27. infrahub/core/diff/merger/merger.py +46 -16
  28. infrahub/core/diff/merger/serializer.py +1 -0
  29. infrahub/core/diff/model/field_specifiers_map.py +64 -0
  30. infrahub/core/diff/model/path.py +58 -58
  31. infrahub/core/diff/parent_node_adder.py +14 -16
  32. infrahub/core/diff/query/all_conflicts.py +1 -5
  33. infrahub/core/diff/query/artifact.py +10 -20
  34. infrahub/core/diff/query/diff_get.py +3 -6
  35. infrahub/core/diff/query/drop_nodes.py +42 -0
  36. infrahub/core/diff/query/field_specifiers.py +8 -7
  37. infrahub/core/diff/query/field_summary.py +2 -4
  38. infrahub/core/diff/query/filters.py +15 -1
  39. infrahub/core/diff/query/merge.py +284 -101
  40. infrahub/core/diff/query/save.py +26 -34
  41. infrahub/core/diff/query/summary_counts_enricher.py +34 -54
  42. infrahub/core/diff/query_parser.py +55 -65
  43. infrahub/core/diff/repository/deserializer.py +38 -24
  44. infrahub/core/diff/repository/repository.py +31 -12
  45. infrahub/core/diff/tasks.py +3 -3
  46. infrahub/core/graph/__init__.py +1 -1
  47. infrahub/core/manager.py +14 -11
  48. infrahub/core/migrations/graph/__init__.py +2 -0
  49. infrahub/core/migrations/graph/m003_relationship_parent_optional.py +1 -2
  50. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +2 -4
  51. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +11 -22
  52. infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -6
  53. infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +1 -2
  54. infrahub/core/migrations/graph/m024_missing_hierarchy_backfill.py +1 -2
  55. infrahub/core/migrations/graph/m027_delete_isolated_nodes.py +50 -0
  56. infrahub/core/migrations/graph/m028_delete_diffs.py +38 -0
  57. infrahub/core/migrations/query/attribute_add.py +1 -2
  58. infrahub/core/migrations/query/attribute_rename.py +3 -6
  59. infrahub/core/migrations/query/delete_element_in_schema.py +3 -6
  60. infrahub/core/migrations/query/node_duplicate.py +3 -6
  61. infrahub/core/migrations/query/relationship_duplicate.py +3 -6
  62. infrahub/core/migrations/schema/node_attribute_remove.py +3 -6
  63. infrahub/core/migrations/schema/node_remove.py +3 -6
  64. infrahub/core/models.py +29 -2
  65. infrahub/core/node/__init__.py +18 -4
  66. infrahub/core/node/create.py +211 -0
  67. infrahub/core/protocols.py +51 -0
  68. infrahub/core/protocols_base.py +3 -0
  69. infrahub/core/query/__init__.py +2 -2
  70. infrahub/core/query/branch.py +27 -17
  71. infrahub/core/query/diff.py +186 -81
  72. infrahub/core/query/ipam.py +10 -20
  73. infrahub/core/query/node.py +65 -49
  74. infrahub/core/query/relationship.py +156 -58
  75. infrahub/core/query/resource_manager.py +1 -2
  76. infrahub/core/query/subquery.py +4 -6
  77. infrahub/core/relationship/model.py +4 -1
  78. infrahub/core/schema/__init__.py +2 -1
  79. infrahub/core/schema/attribute_parameters.py +36 -0
  80. infrahub/core/schema/attribute_schema.py +83 -8
  81. infrahub/core/schema/basenode_schema.py +25 -1
  82. infrahub/core/schema/definitions/core/__init__.py +21 -0
  83. infrahub/core/schema/definitions/internal.py +13 -3
  84. infrahub/core/schema/generated/attribute_schema.py +9 -3
  85. infrahub/core/schema/schema_branch.py +15 -7
  86. infrahub/core/validators/__init__.py +5 -1
  87. infrahub/core/validators/attribute/choices.py +1 -2
  88. infrahub/core/validators/attribute/enum.py +1 -2
  89. infrahub/core/validators/attribute/kind.py +1 -2
  90. infrahub/core/validators/attribute/length.py +13 -6
  91. infrahub/core/validators/attribute/optional.py +1 -2
  92. infrahub/core/validators/attribute/regex.py +5 -5
  93. infrahub/core/validators/attribute/unique.py +1 -3
  94. infrahub/core/validators/determiner.py +18 -2
  95. infrahub/core/validators/enum.py +7 -0
  96. infrahub/core/validators/node/hierarchy.py +3 -6
  97. infrahub/core/validators/query.py +1 -3
  98. infrahub/core/validators/relationship/count.py +6 -12
  99. infrahub/core/validators/relationship/optional.py +2 -4
  100. infrahub/core/validators/relationship/peer.py +3 -8
  101. infrahub/core/validators/tasks.py +1 -1
  102. infrahub/core/validators/uniqueness/query.py +12 -9
  103. infrahub/database/__init__.py +1 -3
  104. infrahub/events/group_action.py +1 -0
  105. infrahub/graphql/analyzer.py +139 -18
  106. infrahub/graphql/app.py +1 -1
  107. infrahub/graphql/loaders/node.py +1 -1
  108. infrahub/graphql/loaders/peers.py +1 -1
  109. infrahub/graphql/manager.py +4 -0
  110. infrahub/graphql/mutations/action.py +164 -0
  111. infrahub/graphql/mutations/convert_object_type.py +62 -0
  112. infrahub/graphql/mutations/main.py +24 -175
  113. infrahub/graphql/mutations/proposed_change.py +21 -18
  114. infrahub/graphql/queries/convert_object_type_mapping.py +36 -0
  115. infrahub/graphql/queries/diff/tree.py +2 -1
  116. infrahub/graphql/queries/relationship.py +1 -1
  117. infrahub/graphql/resolvers/many_relationship.py +4 -4
  118. infrahub/graphql/resolvers/resolver.py +4 -4
  119. infrahub/graphql/resolvers/single_relationship.py +2 -2
  120. infrahub/graphql/schema.py +6 -0
  121. infrahub/graphql/subscription/graphql_query.py +2 -2
  122. infrahub/graphql/types/branch.py +1 -1
  123. infrahub/menu/menu.py +31 -0
  124. infrahub/message_bus/messages/__init__.py +0 -10
  125. infrahub/message_bus/operations/__init__.py +0 -8
  126. infrahub/message_bus/operations/refresh/registry.py +1 -1
  127. infrahub/patch/queries/consolidate_duplicated_nodes.py +3 -6
  128. infrahub/patch/queries/delete_duplicated_edges.py +5 -10
  129. infrahub/prefect_server/models.py +1 -19
  130. infrahub/proposed_change/models.py +68 -3
  131. infrahub/proposed_change/tasks.py +907 -30
  132. infrahub/task_manager/models.py +10 -6
  133. infrahub/telemetry/database.py +1 -1
  134. infrahub/telemetry/tasks.py +1 -1
  135. infrahub/trigger/catalogue.py +2 -0
  136. infrahub/trigger/models.py +29 -3
  137. infrahub/trigger/setup.py +51 -15
  138. infrahub/trigger/tasks.py +4 -5
  139. infrahub/types.py +1 -1
  140. infrahub/webhook/models.py +2 -1
  141. infrahub/workflows/catalogue.py +85 -0
  142. infrahub/workflows/initialization.py +1 -3
  143. infrahub_sdk/timestamp.py +2 -2
  144. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/METADATA +4 -4
  145. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/RECORD +153 -146
  146. infrahub_testcontainers/container.py +0 -1
  147. infrahub_testcontainers/docker-compose.test.yml +4 -4
  148. infrahub_testcontainers/helpers.py +8 -2
  149. infrahub_testcontainers/performance_test.py +6 -3
  150. infrahub/message_bus/messages/check_generator_run.py +0 -26
  151. infrahub/message_bus/messages/finalize_validator_execution.py +0 -15
  152. infrahub/message_bus/messages/proposed_change/base_with_diff.py +0 -16
  153. infrahub/message_bus/messages/proposed_change/request_proposedchange_refreshartifacts.py +0 -11
  154. infrahub/message_bus/messages/request_generatordefinition_check.py +0 -20
  155. infrahub/message_bus/messages/request_proposedchange_pipeline.py +0 -23
  156. infrahub/message_bus/operations/check/__init__.py +0 -3
  157. infrahub/message_bus/operations/check/generator.py +0 -156
  158. infrahub/message_bus/operations/finalize/__init__.py +0 -3
  159. infrahub/message_bus/operations/finalize/validator.py +0 -133
  160. infrahub/message_bus/operations/requests/__init__.py +0 -9
  161. infrahub/message_bus/operations/requests/generator_definition.py +0 -140
  162. infrahub/message_bus/operations/requests/proposed_change.py +0 -629
  163. /infrahub/{message_bus/messages/proposed_change → actions}/__init__.py +0 -0
  164. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/LICENSE.txt +0 -0
  165. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/WHEEL +0 -0
  166. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,122 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from infrahub.core.attribute import BaseAttribute
6
+ from infrahub.core.branch import Branch
7
+ from infrahub.core.constants import RelationshipCardinality
8
+ from infrahub.core.manager import NodeManager
9
+ from infrahub.core.node import Node
10
+ from infrahub.core.node.create import create_node
11
+ from infrahub.core.query.relationship import GetAllPeersIds
12
+ from infrahub.core.relationship import RelationshipManager
13
+ from infrahub.core.schema import NodeSchema
14
+ from infrahub.database import InfrahubDatabase
15
+
16
+
17
+ class InputDataForDestField(BaseModel): # Only one of these fields can be not None
18
+ attribute_value: Any | None = None
19
+ peer_id: str | None = None
20
+ peers_ids: list[str] | None = None
21
+
22
+ @property
23
+ def value(self) -> Any:
24
+ fields = [self.attribute_value, self.peer_id, self.peers_ids]
25
+ set_fields = [f for f in fields if f is not None]
26
+ if len(set_fields) != 1:
27
+ raise ValueError("Exactly one of attribute_value, peer_id, or peers_ids must be set")
28
+ return set_fields[0]
29
+
30
+
31
+ class InputForDestField(BaseModel): # Only one of these fields can be not None
32
+ source_field: str | None = None
33
+ data: InputDataForDestField | None = None
34
+
35
+ @property
36
+ def value(self) -> Any:
37
+ if self.source_field is not None and self.data is not None:
38
+ raise ValueError("Only one of source_field or data can be set")
39
+ if self.source_field is None and self.data is None:
40
+ raise ValueError("Either source_field or data must be set")
41
+ return self.source_field if self.source_field is not None else self.data
42
+
43
+
44
+ async def get_out_rels_peers_ids(node: Node, db: InfrahubDatabase) -> list[str]:
45
+ all_peers: list[Node] = []
46
+ for name in node._relationships:
47
+ relm: RelationshipManager = getattr(node, name)
48
+ peers = await relm.get_peers(db=db)
49
+ all_peers.extend(peers.values())
50
+ return [peer.id for peer in all_peers]
51
+
52
+
53
+ async def build_data_new_node(db: InfrahubDatabase, mapping: dict[str, InputForDestField], node: Node) -> dict:
54
+ """Value of a given field on the target kind to convert is either an input source attribute/relationship of the source node,
55
+ or a raw value."""
56
+
57
+ data = {}
58
+ for dest_field_name, input_for_dest_field in mapping.items():
59
+ value = input_for_dest_field.value
60
+ if isinstance(value, str): # source_field
61
+ item = getattr(node, value)
62
+ if isinstance(item, BaseAttribute):
63
+ data[dest_field_name] = item.value
64
+ elif isinstance(item, RelationshipManager):
65
+ if item.schema.cardinality == RelationshipCardinality.ONE:
66
+ peer = await item.get_peer(db=db)
67
+ if peer is not None:
68
+ data[dest_field_name] = {"id": peer.id}
69
+ # else, relationship is optional, and if the target relationship is mandatory an error will be raised during creation
70
+ elif item.schema.cardinality == RelationshipCardinality.MANY:
71
+ data[dest_field_name] = [{"id": peer.id} for _, peer in (await item.get_peers(db=db)).items()]
72
+ else:
73
+ raise ValueError(f"Unknown cardinality {item.schema.cardinality=}")
74
+ else: # user input data
75
+ data[dest_field_name] = value.value
76
+ return data
77
+
78
+
79
+ async def get_unidirectional_rels_peers_ids(node: Node, branch: Branch, db: InfrahubDatabase) -> list[str]:
80
+ """
81
+ Returns peers ids of nodes connected to input `node` through an incoming unidirectional relationship.
82
+ """
83
+
84
+ out_rels_identifier = [rel.identifier for rel in node.get_schema().relationships]
85
+ query = await GetAllPeersIds.init(db=db, node_id=node.id, branch=branch, exclude_identifiers=out_rels_identifier)
86
+ await query.execute(db=db)
87
+ return query.get_peers_uuids()
88
+
89
+
90
+ async def convert_object_type(
91
+ node: Node, target_schema: NodeSchema, mapping: dict[str, InputForDestField], branch: Branch, db: InfrahubDatabase
92
+ ) -> Node:
93
+ """Delete the node and return the new created one. If creation fails, the node is not deleted, and raise an error.
94
+ An extra check is performed on input node peers relationships to make sure they are still valid."""
95
+
96
+ node_schema = node.get_schema()
97
+ if not isinstance(node_schema, NodeSchema):
98
+ raise ValueError(f"Only a node with a NodeSchema can be converted, got {type(node_schema)}")
99
+
100
+ async with db.start_transaction() as dbt: # noqa: PLR1702
101
+ deleted_node_out_rels_peer_ids = await get_out_rels_peers_ids(node=node, db=dbt)
102
+ deleted_node_unidir_rels_peer_ids = await get_unidirectional_rels_peers_ids(node=node, db=dbt, branch=branch)
103
+
104
+ deleted_nodes = await NodeManager.delete(db=dbt, branch=branch, nodes=[node], cascade_delete=False)
105
+ if len(deleted_nodes) != 1:
106
+ raise ValueError(f"Deleted {len(deleted_nodes)} nodes instead of 1")
107
+
108
+ data_new_node = await build_data_new_node(dbt, mapping, node)
109
+ node_created = await create_node(
110
+ data=data_new_node,
111
+ db=dbt,
112
+ branch=branch,
113
+ schema=target_schema,
114
+ )
115
+
116
+ # Make sure relationships with constraints are not broken by retrieving them
117
+ peers_ids = deleted_node_out_rels_peer_ids + deleted_node_unidir_rels_peer_ids
118
+ peers = await NodeManager.get_many(ids=peers_ids, db=dbt, prefetch_relationships=True, branch=branch)
119
+ for peer in peers.values():
120
+ peer.validate_relationships()
121
+
122
+ return node_created
@@ -0,0 +1,56 @@
1
+ from pydantic import BaseModel
2
+
3
+ from infrahub.core.constants import RelationshipCardinality
4
+ from infrahub.core.schema import NodeSchema
5
+
6
+
7
+ class SchemaMappingValue(BaseModel):
8
+ is_mandatory: bool
9
+ source_field_name: str | None = None # None means there is no corresponding source field name
10
+ relationship_cardinality: RelationshipCardinality | None = None
11
+
12
+
13
+ SchemaMapping = dict[str, SchemaMappingValue]
14
+
15
+
16
+ def get_schema_mapping(source_schema: NodeSchema, target_schema: NodeSchema) -> SchemaMapping:
17
+ """
18
+ Return fields mapping meant to be used for converting a node from `source_kind` to `target_kind`.
19
+ For any field of the target kind, field of the source kind will be matched if:
20
+ - It's an attribute with identical name and type.
21
+ - It's a relationship with identical peer kind and cardinality.
22
+ If there is no match, the mapping will only indicate whether the field is mandatory or not.
23
+ """
24
+
25
+ target_field_to_source_field = {}
26
+
27
+ # Create lookup dictionaries for source attributes and relationships
28
+ source_attrs = {attr.name: attr for attr in source_schema.attributes}
29
+ source_rels = {rel.name: rel for rel in source_schema.relationships}
30
+
31
+ # Process attributes
32
+ for target_attr in target_schema.attributes:
33
+ source_attr = source_attrs.get(target_attr.name)
34
+ if source_attr and source_attr.kind == target_attr.kind:
35
+ target_field_to_source_field[target_attr.name] = SchemaMappingValue(
36
+ source_field_name=source_attr.name, is_mandatory=not target_attr.optional
37
+ )
38
+ else:
39
+ target_field_to_source_field[target_attr.name] = SchemaMappingValue(is_mandatory=not target_attr.optional)
40
+
41
+ # Process relationships
42
+ for target_rel in target_schema.relationships:
43
+ source_rel = source_rels.get(target_rel.name)
44
+ if source_rel and source_rel.peer == target_rel.peer and source_rel.cardinality == target_rel.cardinality:
45
+ target_field_to_source_field[target_rel.name] = SchemaMappingValue(
46
+ source_field_name=source_rel.name,
47
+ is_mandatory=not target_rel.optional,
48
+ relationship_cardinality=target_rel.cardinality,
49
+ )
50
+ else:
51
+ target_field_to_source_field[target_rel.name] = SchemaMappingValue(
52
+ is_mandatory=not target_rel.optional,
53
+ relationship_cardinality=target_rel.cardinality,
54
+ )
55
+
56
+ return target_field_to_source_field
@@ -7,6 +7,7 @@ from infrahub.core.diff.query_parser import DiffQueryParser
7
7
  from infrahub.core.query.diff import (
8
8
  DiffCalculationQuery,
9
9
  DiffFieldPathsQuery,
10
+ DiffMigratedKindNodesQuery,
10
11
  DiffNodePathsQuery,
11
12
  DiffPropertyPathsQuery,
12
13
  )
@@ -14,7 +15,8 @@ from infrahub.core.timestamp import Timestamp
14
15
  from infrahub.database import InfrahubDatabase
15
16
  from infrahub.log import get_logger
16
17
 
17
- from .model.path import CalculatedDiffs
18
+ from .model.field_specifiers_map import NodeFieldSpecifierMap
19
+ from .model.path import CalculatedDiffs, DiffNode, DiffRoot, NodeIdentifier
18
20
 
19
21
  log = get_logger()
20
22
 
@@ -26,8 +28,8 @@ class DiffCalculationRequest:
26
28
  branch_from_time: Timestamp
27
29
  from_time: Timestamp
28
30
  to_time: Timestamp
29
- current_node_field_specifiers: dict[str, set[str]] | None = field(default=None)
30
- new_node_field_specifiers: dict[str, set[str]] | None = field(default=None)
31
+ current_node_field_specifiers: NodeFieldSpecifierMap | None = field(default=None)
32
+ new_node_field_specifiers: NodeFieldSpecifierMap | None = field(default=None)
31
33
 
32
34
 
33
35
  class DiffCalculator:
@@ -58,7 +60,7 @@ class DiffCalculator:
58
60
  )
59
61
  log.info(f"Beginning one diff calculation query {limit=}, {offset=}")
60
62
  await diff_query.execute(db=self.db)
61
- log.info("Diff calculation query complete")
63
+ log.info(f"Diff calculation query complete {limit=}, {offset=}")
62
64
  last_result = None
63
65
  for query_result in diff_query.get_results():
64
66
  diff_parser.read_result(query_result=query_result)
@@ -68,6 +70,56 @@ class DiffCalculator:
68
70
  has_more_data = last_result.get_as_type("has_more_data", bool)
69
71
  offset += limit
70
72
 
73
+ async def _apply_kind_migrated_nodes(
74
+ self, branch_diff: DiffRoot, calculation_request: DiffCalculationRequest
75
+ ) -> None:
76
+ has_more_data = True
77
+ offset = 0
78
+ limit = config.SETTINGS.database.query_size_limit
79
+ diff_nodes_by_identifier = {n.identifier: n for n in branch_diff.nodes}
80
+ diff_nodes_to_add: list[DiffNode] = []
81
+ while has_more_data:
82
+ diff_query = await DiffMigratedKindNodesQuery.init(
83
+ db=self.db,
84
+ branch=calculation_request.diff_branch,
85
+ base_branch=calculation_request.base_branch,
86
+ diff_branch_from_time=calculation_request.branch_from_time,
87
+ diff_from=calculation_request.from_time,
88
+ diff_to=calculation_request.to_time,
89
+ limit=limit,
90
+ offset=offset,
91
+ )
92
+ log.info(f"Getting one batch of migrated kind nodes {limit=}, {offset=}")
93
+ await diff_query.execute(db=self.db)
94
+ log.info(f"Migrated kind nodes query complete {limit=}, {offset=}")
95
+ last_result = None
96
+ for migrated_kind_node in diff_query.get_migrated_kind_nodes():
97
+ migrated_kind_identifier = NodeIdentifier(
98
+ uuid=migrated_kind_node.uuid,
99
+ kind=migrated_kind_node.kind,
100
+ db_id=migrated_kind_node.db_id,
101
+ )
102
+ if migrated_kind_identifier in diff_nodes_by_identifier:
103
+ diff_node = diff_nodes_by_identifier[migrated_kind_identifier]
104
+ diff_node.is_node_kind_migration = True
105
+ continue
106
+ new_diff_node = DiffNode(
107
+ identifier=migrated_kind_identifier,
108
+ changed_at=migrated_kind_node.from_time,
109
+ action=migrated_kind_node.action,
110
+ is_node_kind_migration=True,
111
+ attributes=[],
112
+ relationships=[],
113
+ )
114
+ diff_nodes_by_identifier[migrated_kind_identifier] = new_diff_node
115
+ diff_nodes_to_add.append(new_diff_node)
116
+ last_result = migrated_kind_node
117
+ has_more_data = False
118
+ if last_result:
119
+ has_more_data = last_result.has_more_data
120
+ offset += limit
121
+ branch_diff.nodes.extend(diff_nodes_to_add)
122
+
71
123
  async def calculate_diff(
72
124
  self,
73
125
  base_branch: Branch,
@@ -75,7 +127,7 @@ class DiffCalculator:
75
127
  from_time: Timestamp,
76
128
  to_time: Timestamp,
77
129
  include_unchanged: bool = True,
78
- previous_node_specifiers: dict[str, set[str]] | None = None,
130
+ previous_node_specifiers: NodeFieldSpecifierMap | None = None,
79
131
  ) -> CalculatedDiffs:
80
132
  if diff_branch.name == registry.default_branch:
81
133
  diff_branch_from_time = from_time
@@ -91,7 +143,7 @@ class DiffCalculator:
91
143
  )
92
144
  node_limit = int(config.SETTINGS.database.query_size_limit / 10)
93
145
  fields_limit = int(config.SETTINGS.database.query_size_limit / 3)
94
- properties_limit = int(config.SETTINGS.database.query_size_limit)
146
+ properties_limit = config.SETTINGS.database.query_size_limit
95
147
 
96
148
  calculation_request = DiffCalculationRequest(
97
149
  base_branch=base_branch,
@@ -131,7 +183,7 @@ class DiffCalculator:
131
183
  if base_branch.name != diff_branch.name:
132
184
  current_node_field_specifiers = diff_parser.get_current_node_field_specifiers()
133
185
  new_node_field_specifiers = diff_parser.get_new_node_field_specifiers()
134
- calculation_request = DiffCalculationRequest(
186
+ base_calculation_request = DiffCalculationRequest(
135
187
  base_branch=base_branch,
136
188
  diff_branch=base_branch,
137
189
  branch_from_time=diff_branch_from_time,
@@ -145,7 +197,7 @@ class DiffCalculator:
145
197
  await self._run_diff_calculation_query(
146
198
  diff_parser=diff_parser,
147
199
  query_class=DiffNodePathsQuery,
148
- calculation_request=calculation_request,
200
+ calculation_request=base_calculation_request,
149
201
  limit=node_limit,
150
202
  )
151
203
  log.info("Diff node-level calculation queries for base complete")
@@ -154,7 +206,7 @@ class DiffCalculator:
154
206
  await self._run_diff_calculation_query(
155
207
  diff_parser=diff_parser,
156
208
  query_class=DiffFieldPathsQuery,
157
- calculation_request=calculation_request,
209
+ calculation_request=base_calculation_request,
158
210
  limit=fields_limit,
159
211
  )
160
212
  log.info("Diff field-level calculation queries for base complete")
@@ -163,7 +215,7 @@ class DiffCalculator:
163
215
  await self._run_diff_calculation_query(
164
216
  diff_parser=diff_parser,
165
217
  query_class=DiffPropertyPathsQuery,
166
- calculation_request=calculation_request,
218
+ calculation_request=base_calculation_request,
167
219
  limit=properties_limit,
168
220
  )
169
221
  log.info("Diff property-level calculation queries for base complete")
@@ -171,9 +223,11 @@ class DiffCalculator:
171
223
  log.info("Parsing calculated diff")
172
224
  diff_parser.parse(include_unchanged=include_unchanged)
173
225
  log.info("Calculated diff parsed")
226
+ branch_diff = diff_parser.get_diff_root_for_branch(branch=diff_branch.name)
227
+ await self._apply_kind_migrated_nodes(branch_diff=branch_diff, calculation_request=calculation_request)
174
228
  return CalculatedDiffs(
175
229
  base_branch_name=base_branch.name,
176
230
  diff_branch_name=diff_branch.name,
177
231
  base_branch_diff=diff_parser.get_diff_root_for_branch(branch=base_branch.name),
178
- diff_branch_diff=diff_parser.get_diff_root_for_branch(branch=diff_branch.name),
232
+ diff_branch_diff=branch_diff,
179
233
  )
@@ -14,6 +14,7 @@ from .model.path import (
14
14
  EnrichedDiffRoot,
15
15
  EnrichedDiffs,
16
16
  EnrichedDiffSingleRelationship,
17
+ NodeIdentifier,
17
18
  )
18
19
 
19
20
 
@@ -26,30 +27,35 @@ class NodePair:
26
27
  class DiffCombiner:
27
28
  def __init__(self) -> None:
28
29
  # {child_uuid: (parent_uuid, parent_rel_name)}
29
- self._child_parent_uuid_map: dict[str, tuple[str, str]] = {}
30
- self._parent_node_uuids: set[str] = set()
31
- self._earlier_nodes_by_uuid: dict[str, EnrichedDiffNode] = {}
32
- self._later_nodes_by_uuid: dict[str, EnrichedDiffNode] = {}
33
- self._common_node_uuids: set[str] = set()
30
+ self._child_parent_identifier_map: dict[NodeIdentifier, tuple[NodeIdentifier, str]] = {}
31
+ self._parent_node_identifiers: set[NodeIdentifier] = set()
32
+ self._earlier_nodes_by_identifier: dict[NodeIdentifier, EnrichedDiffNode] = {}
33
+ self._later_nodes_by_identifier: dict[NodeIdentifier, EnrichedDiffNode] = {}
34
+ self._common_node_identifiers: set[NodeIdentifier] = set()
34
35
  self._diff_branch_name: str | None = None
35
36
 
36
37
  def _initialize(self, earlier_diff: EnrichedDiffRoot, later_diff: EnrichedDiffRoot) -> None:
37
38
  self._diff_branch_name = earlier_diff.diff_branch_name
38
- self._child_parent_uuid_map = {}
39
- self._earlier_nodes_by_uuid = {}
40
- self._later_nodes_by_uuid = {}
41
- self._common_node_uuids = set()
39
+ self._child_parent_identifier_map = {}
40
+ self._earlier_nodes_by_identifier = {}
41
+ self._later_nodes_by_identifier = {}
42
+ self._common_node_identifiers = set()
42
43
  # map the parent of each node (if it exists), preference to the later diff
43
44
  for diff_root in (earlier_diff, later_diff):
44
45
  for child_node in diff_root.nodes:
45
46
  for parent_rel in child_node.relationships:
46
47
  for parent_node in parent_rel.nodes:
47
- self._child_parent_uuid_map[child_node.uuid] = (parent_node.uuid, parent_rel.name)
48
+ self._child_parent_identifier_map[child_node.identifier] = (
49
+ parent_node.identifier,
50
+ parent_rel.name,
51
+ )
48
52
  # UUIDs of all the parents, removing the stale parents from the earlier diff
49
- self._parent_node_uuids = {parent_tuple[0] for parent_tuple in self._child_parent_uuid_map.values()}
50
- self._earlier_nodes_by_uuid = {n.uuid: n for n in earlier_diff.nodes}
51
- self._later_nodes_by_uuid = {n.uuid: n for n in later_diff.nodes}
52
- self._common_node_uuids = set(self._earlier_nodes_by_uuid.keys()) & set(self._later_nodes_by_uuid.keys())
53
+ self._parent_node_identifiers = {parent_tuple[0] for parent_tuple in self._child_parent_identifier_map.values()}
54
+ self._earlier_nodes_by_identifier = {n.identifier: n for n in earlier_diff.nodes}
55
+ self._later_nodes_by_identifier = {n.identifier: n for n in later_diff.nodes}
56
+ self._common_node_identifiers = set(self._earlier_nodes_by_identifier.keys()) & set(
57
+ self._later_nodes_by_identifier.keys()
58
+ )
53
59
 
54
60
  @property
55
61
  def diff_branch_name(self) -> str:
@@ -61,13 +67,13 @@ class DiffCombiner:
61
67
  filtered_node_pairs: list[NodePair] = []
62
68
  for earlier_node in earlier_diff.nodes:
63
69
  later_node: EnrichedDiffNode | None = None
64
- if earlier_node.uuid in self._common_node_uuids:
65
- later_node = self._later_nodes_by_uuid[earlier_node.uuid]
70
+ if earlier_node.identifier in self._common_node_identifiers:
71
+ later_node = self._later_nodes_by_identifier[earlier_node.identifier]
66
72
  # this is an out-of-date parent
67
73
  if (
68
74
  earlier_node.action is DiffAction.UNCHANGED
69
75
  and (later_node is None or later_node.action is DiffAction.UNCHANGED)
70
- and earlier_node.uuid not in self._parent_node_uuids
76
+ and earlier_node.identifier not in self._parent_node_identifiers
71
77
  ):
72
78
  continue
73
79
  if later_node is None:
@@ -79,15 +85,15 @@ class DiffCombiner:
79
85
  filtered_node_pairs.append(NodePair(earlier=earlier_node, later=later_node))
80
86
  for later_node in later_diff.nodes:
81
87
  # these have already been handled
82
- if later_node.uuid in self._common_node_uuids:
88
+ if later_node.identifier in self._common_node_identifiers:
83
89
  continue
84
90
  filtered_node_pairs.append(NodePair(later=later_node))
85
91
  return filtered_node_pairs
86
92
 
87
- def _get_parent_relationship_name(self, node_id: str) -> str | None:
88
- if node_id not in self._child_parent_uuid_map:
93
+ def _get_parent_relationship_name(self, node_id: NodeIdentifier) -> str | None:
94
+ if node_id not in self._child_parent_identifier_map:
89
95
  return None
90
- return self._child_parent_uuid_map[node_id][1]
96
+ return self._child_parent_identifier_map[node_id][1]
91
97
 
92
98
  def _should_include(self, earlier: DiffAction, later: DiffAction) -> bool:
93
99
  actions = {earlier, later}
@@ -284,7 +290,7 @@ class DiffCombiner:
284
290
  self,
285
291
  earlier_relationships: set[EnrichedDiffRelationship],
286
292
  later_relationships: set[EnrichedDiffRelationship],
287
- node_id: str,
293
+ node_id: NodeIdentifier,
288
294
  ) -> set[EnrichedDiffRelationship]:
289
295
  earlier_rels_by_name = {rel.name: rel for rel in earlier_relationships}
290
296
  later_rels_by_name = {rel.name: rel for rel in later_relationships}
@@ -365,7 +371,7 @@ class DiffCombiner:
365
371
  combined_relationships = self._combine_relationships(
366
372
  earlier_relationships=node_pair.earlier.relationships,
367
373
  later_relationships=node_pair.later.relationships,
368
- node_id=node_pair.later.uuid,
374
+ node_id=node_pair.later.identifier,
369
375
  )
370
376
  if all(ca.action is DiffAction.UNCHANGED for ca in combined_attributes) and all(
371
377
  cr.action is DiffAction.UNCHANGED for cr in combined_relationships
@@ -380,15 +386,16 @@ class DiffCombiner:
380
386
  combined_attributes
381
387
  or combined_relationships
382
388
  or combined_conflict
383
- or node_pair.later.uuid in self._parent_node_uuids
389
+ or node_pair.later.identifier in self._parent_node_identifiers
384
390
  ):
385
391
  combined_nodes.add(
386
392
  EnrichedDiffNode(
387
- uuid=node_pair.later.uuid,
388
- kind=node_pair.later.kind,
393
+ identifier=node_pair.later.identifier,
389
394
  label=node_pair.later.label,
390
395
  changed_at=node_pair.later.changed_at or node_pair.earlier.changed_at,
391
396
  action=combined_action,
397
+ is_node_kind_migration=node_pair.earlier.is_node_kind_migration
398
+ or node_pair.later.is_node_kind_migration,
392
399
  path_identifier=node_pair.later.path_identifier,
393
400
  attributes=combined_attributes,
394
401
  relationships=combined_relationships,
@@ -398,12 +405,12 @@ class DiffCombiner:
398
405
  return combined_nodes
399
406
 
400
407
  def _link_child_nodes(self, nodes: Iterable[EnrichedDiffNode]) -> None:
401
- nodes_by_uuid: dict[str, EnrichedDiffNode] = {n.uuid: n for n in nodes}
402
- for child_node in nodes_by_uuid.values():
403
- if child_node.uuid not in self._child_parent_uuid_map:
408
+ nodes_by_identifier: dict[NodeIdentifier, EnrichedDiffNode] = {n.identifier: n for n in nodes}
409
+ for child_node in nodes_by_identifier.values():
410
+ if child_node.identifier not in self._child_parent_identifier_map:
404
411
  continue
405
- parent_uuid, parent_rel_name = self._child_parent_uuid_map[child_node.uuid]
406
- parent_node = nodes_by_uuid[parent_uuid]
412
+ parent_identifier, parent_rel_name = self._child_parent_identifier_map[child_node.identifier]
413
+ parent_node = nodes_by_identifier[parent_identifier]
407
414
  parent_rel = child_node.get_relationship(name=parent_rel_name)
408
415
  parent_rel.nodes.add(parent_node)
409
416