Skip to content

Commit 239b3ba

Browse files
authored
support properties on attributes in the new merge (#4585)
* support properties on attributes * support for attribute properties merge
1 parent d1dfc2f commit 239b3ba

File tree

5 files changed

+364
-54
lines changed

5 files changed

+364
-54
lines changed

backend/infrahub/core/diff/merger/merger.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import TYPE_CHECKING
44

55
from infrahub.core.diff.model.path import BranchTrackingId
6-
from infrahub.core.diff.query.merge import DiffMergeQuery
6+
from infrahub.core.diff.query.merge import DiffMergePropertiesQuery, DiffMergeQuery
77

88
if TYPE_CHECKING:
99
from infrahub.core.branch import Branch
@@ -33,15 +33,23 @@ async def merge_graph(self, at: Timestamp) -> None:
3333
enriched_diff = await self.diff_repository.get_one(
3434
diff_branch_name=self.source_branch.name, tracking_id=BranchTrackingId(name=self.source_branch.name)
3535
)
36-
node_diff_dicts = await self.serializer.serialize(diff=enriched_diff)
37-
merge_query = await DiffMergeQuery.init(
38-
db=self.db,
39-
branch=self.source_branch,
40-
at=at,
41-
target_branch=self.destination_branch,
42-
node_diff_dicts=node_diff_dicts,
43-
)
44-
await merge_query.execute(db=self.db)
36+
async for node_diff_dicts, property_diff_dicts in self.serializer.serialize_diff(diff=enriched_diff):
37+
merge_query = await DiffMergeQuery.init(
38+
db=self.db,
39+
branch=self.source_branch,
40+
at=at,
41+
target_branch=self.destination_branch,
42+
node_diff_dicts=node_diff_dicts,
43+
)
44+
await merge_query.execute(db=self.db)
45+
merge_properties_query = await DiffMergePropertiesQuery.init(
46+
db=self.db,
47+
branch=self.source_branch,
48+
at=at,
49+
target_branch=self.destination_branch,
50+
property_diff_dicts=property_diff_dicts,
51+
)
52+
await merge_properties_query.execute(db=self.db)
4553

4654
self.source_branch.branched_from = at.to_string()
4755
await self.source_branch.save(db=self.db)

backend/infrahub/core/diff/merger/model.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing_extensions import TypedDict
1+
from typing import TypedDict
22

33

44
class RelationshipMergeDict(TypedDict):
@@ -17,3 +17,22 @@ class NodeMergeDict(TypedDict):
1717
action: str
1818
attributes: list[AttributeMergeDict]
1919
relationships: list[RelationshipMergeDict]
20+
21+
22+
class PropertyMergeDict(TypedDict):
23+
property_type: str
24+
action: str
25+
value: str | bool | int | float | None
26+
27+
28+
class AttributePropertyMergeDict(TypedDict):
29+
node_uuid: str
30+
attribute_name: str
31+
properties: list[PropertyMergeDict]
32+
33+
34+
class RelationshipPropertyMergeDict(TypedDict):
35+
node_uuid: str
36+
relationship_id: str
37+
peer_uuid: str
38+
properties: list[PropertyMergeDict]
Lines changed: 141 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,40 @@
1+
from typing import AsyncGenerator
2+
13
from infrahub.core.constants import DiffAction
24
from infrahub.core.constants.database import DatabaseEdgeType
35
from infrahub.core.schema.schema_branch import SchemaBranch
6+
from infrahub.types import ATTRIBUTE_PYTHON_TYPES
47

58
from ..model.path import (
69
ConflictSelection,
710
EnrichedDiffAttribute,
811
EnrichedDiffConflict,
12+
EnrichedDiffProperty,
913
EnrichedDiffRoot,
1014
EnrichedDiffSingleRelationship,
1115
)
12-
from .model import AttributeMergeDict, NodeMergeDict, RelationshipMergeDict
16+
from .model import (
17+
AttributeMergeDict,
18+
AttributePropertyMergeDict,
19+
NodeMergeDict,
20+
PropertyMergeDict,
21+
RelationshipMergeDict,
22+
RelationshipPropertyMergeDict,
23+
)
24+
25+
Primitives = str | bool | int | float
1326

1427

1528
class DiffMergeSerializer:
16-
def __init__(self, schema_branch: SchemaBranch) -> None:
29+
def __init__(self, schema_branch: SchemaBranch, max_batch_size: int) -> None:
1730
self.schema_branch = schema_branch
31+
self.max_batch_size = max_batch_size
1832
self._relationship_id_cache: dict[tuple[str, str], str] = {}
33+
self._attribute_type_cache: dict[tuple[str, str], type] = {}
34+
35+
def _reset_caches(self) -> None:
36+
self._relationship_id_cache = {}
37+
self._attribute_type_cache = {}
1938

2039
def _get_action(self, action: DiffAction, conflict: EnrichedDiffConflict | None) -> DiffAction:
2140
if not conflict:
@@ -29,32 +48,75 @@ def _get_action(self, action: DiffAction, conflict: EnrichedDiffConflict | None)
2948
def _to_action_str(self, action: DiffAction) -> str:
3049
return str(action.value).upper()
3150

32-
def _get_relationship_identifier(self, schema_kind_str: str, relationship_name: str) -> str:
33-
cache_key = (schema_kind_str, relationship_name)
51+
def _get_relationship_identifier(self, schema_kind: str, relationship_name: str) -> str:
52+
cache_key = (schema_kind, relationship_name)
3453
if cache_key in self._relationship_id_cache:
3554
return self._relationship_id_cache[cache_key]
36-
node_schema = self.schema_branch.get(name=schema_kind_str, duplicate=False)
55+
node_schema = self.schema_branch.get(name=schema_kind, duplicate=False)
3756
relationship_schema = node_schema.get_relationship(name=relationship_name)
3857
relationship_identifier = relationship_schema.get_identifier()
3958
self._relationship_id_cache[cache_key] = relationship_identifier
4059
return relationship_identifier
4160

42-
async def serialize(self, diff: EnrichedDiffRoot) -> list[NodeMergeDict]:
61+
def _get_property_type_for_attribute_value(self, schema_kind: str, attribute_name: str) -> type:
62+
cache_key = (schema_kind, attribute_name)
63+
if cache_key in self._attribute_type_cache:
64+
return self._attribute_type_cache[cache_key]
65+
node_schema = self.schema_branch.get(name=schema_kind, duplicate=False)
66+
attribute_schema = node_schema.get_attribute(name=attribute_name)
67+
python_type = ATTRIBUTE_PYTHON_TYPES[attribute_schema.kind]
68+
final_python_type: type = str
69+
if python_type in (str, int, float, bool):
70+
final_python_type = python_type
71+
self._attribute_type_cache[cache_key] = final_python_type
72+
return final_python_type
73+
74+
def _convert_property_value(
75+
self, property_type: DatabaseEdgeType, raw_value: str | None, value_type: type | None = None
76+
) -> Primitives | None:
77+
if raw_value is None:
78+
if property_type is DatabaseEdgeType.HAS_VALUE:
79+
return "NULL"
80+
return None
81+
# peer IDs are strings
82+
if property_type in (DatabaseEdgeType.HAS_OWNER, DatabaseEdgeType.HAS_SOURCE, DatabaseEdgeType.IS_RELATED):
83+
return raw_value
84+
# these are boolean
85+
if property_type in (DatabaseEdgeType.IS_VISIBLE, DatabaseEdgeType.IS_PROTECTED):
86+
return raw_value.lower() == "true"
87+
# this must be HAS_VALUE
88+
if value_type:
89+
return value_type(raw_value)
90+
return raw_value
91+
92+
async def serialize_diff(
93+
self, diff: EnrichedDiffRoot
94+
) -> AsyncGenerator[
95+
tuple[list[NodeMergeDict], list[AttributePropertyMergeDict | RelationshipPropertyMergeDict]], None
96+
]:
97+
self._reset_caches()
4398
serialized_node_diffs = []
99+
serialized_property_diffs: list[AttributePropertyMergeDict | RelationshipPropertyMergeDict] = []
44100
for node in diff.nodes:
45101
node_action = self._get_action(action=node.action, conflict=node.conflict)
46-
attribute_diffs = [self._serialize_attribute(attribute_diff=attr_diff) for attr_diff in node.attributes]
102+
attribute_diffs = []
103+
for attr_diff in node.attributes:
104+
attribute_diff, attribute_property_diff = self._serialize_attribute(
105+
attribute_diff=attr_diff, node_uuid=node.uuid, node_kind=node.kind
106+
)
107+
attribute_diffs.append(attribute_diff)
108+
serialized_property_diffs.append(attribute_property_diff)
47109
relationship_diffs = []
48110
for rel_diff in node.relationships:
49111
relationship_identifier = self._get_relationship_identifier(
50-
schema_kind_str=node.kind, relationship_name=rel_diff.name
112+
schema_kind=node.kind, relationship_name=rel_diff.name
51113
)
52114
for relationship_element_diff in rel_diff.relationships:
53-
relationship_diffs.extend(
54-
self._serialize_relationship_element(
55-
relationship_diff=relationship_element_diff, relationship_identifier=relationship_identifier
56-
)
115+
element_diffs, relationship_property_diffs = self._serialize_relationship_element(
116+
relationship_diff=relationship_element_diff, relationship_identifier=relationship_identifier
57117
)
118+
relationship_diffs.extend(element_diffs)
119+
serialized_property_diffs.extend(relationship_property_diffs)
58120
serialized_node_diffs.append(
59121
NodeMergeDict(
60122
uuid=node.uuid,
@@ -63,41 +125,82 @@ async def serialize(self, diff: EnrichedDiffRoot) -> list[NodeMergeDict]:
63125
relationships=relationship_diffs,
64126
)
65127
)
66-
return serialized_node_diffs
128+
if len(serialized_node_diffs) == self.max_batch_size:
129+
yield (serialized_node_diffs, serialized_property_diffs)
130+
serialized_node_diffs, serialized_property_diffs = [], []
131+
yield (serialized_node_diffs, serialized_property_diffs)
132+
133+
def _get_property_actions_and_values(
134+
self, property_diff: EnrichedDiffProperty, python_value_type: type
135+
) -> list[tuple[DiffAction, Primitives]]:
136+
action = property_diff.action
137+
new_value = property_diff.new_value
138+
if property_diff.conflict and property_diff.conflict.selected_branch is ConflictSelection.BASE_BRANCH:
139+
action = property_diff.conflict.base_branch_action
140+
if property_diff.conflict.base_branch_value:
141+
new_value = property_diff.conflict.base_branch_value
142+
actions = [action]
143+
if property_diff.action is DiffAction.UPDATED:
144+
actions = [DiffAction.ADDED, DiffAction.REMOVED]
145+
actions_and_values: list[tuple[DiffAction, Primitives]] = []
146+
for action in actions:
147+
if action not in (DiffAction.ADDED, DiffAction.REMOVED):
148+
continue
149+
if action is DiffAction.ADDED:
150+
raw_value = new_value
151+
else:
152+
raw_value = property_diff.previous_value
153+
final_value = self._convert_property_value(
154+
property_type=property_diff.property_type, raw_value=raw_value, value_type=python_value_type
155+
)
156+
if final_value:
157+
actions_and_values.append((action, final_value))
158+
return actions_and_values
67159

68-
def _serialize_attribute(self, attribute_diff: EnrichedDiffAttribute) -> AttributeMergeDict:
69-
return AttributeMergeDict(
160+
def _serialize_attribute(
161+
self, attribute_diff: EnrichedDiffAttribute, node_uuid: str, node_kind: str
162+
) -> tuple[AttributeMergeDict, AttributePropertyMergeDict]:
163+
prop_dicts: list[PropertyMergeDict] = []
164+
python_type = self._get_property_type_for_attribute_value(
165+
schema_kind=node_kind, attribute_name=attribute_diff.name
166+
)
167+
for property_diff in attribute_diff.properties:
168+
actions_and_values = self._get_property_actions_and_values(
169+
property_diff=property_diff, python_value_type=python_type
170+
)
171+
for action, value in actions_and_values:
172+
prop_dicts.append(
173+
PropertyMergeDict(
174+
property_type=property_diff.property_type.value,
175+
action=self._to_action_str(action=action),
176+
value=value,
177+
)
178+
)
179+
attr_dict = AttributeMergeDict(
70180
name=attribute_diff.name,
71181
action=self._to_action_str(action=attribute_diff.action),
72182
)
183+
attr_prop_dict = AttributePropertyMergeDict(
184+
node_uuid=node_uuid, attribute_name=attribute_diff.name, properties=prop_dicts
185+
)
186+
return attr_dict, attr_prop_dict
73187

74188
def _serialize_relationship_element(
75189
self, relationship_diff: EnrichedDiffSingleRelationship, relationship_identifier: str
76-
) -> list[RelationshipMergeDict]:
190+
) -> tuple[list[RelationshipMergeDict], list[RelationshipPropertyMergeDict]]:
77191
relationship_dicts = []
78192
for property_diff in relationship_diff.properties:
79193
if property_diff.property_type is not DatabaseEdgeType.IS_RELATED:
80194
continue
81-
action = property_diff.action
82-
new_value = relationship_diff.peer_id
83-
if property_diff.conflict and property_diff.conflict.selected_branch is ConflictSelection.BASE_BRANCH:
84-
action = property_diff.conflict.base_branch_action
85-
if property_diff.conflict.base_branch_value:
86-
new_value = property_diff.conflict.base_branch_value
87-
actions = [action]
88-
if property_diff.action is DiffAction.UPDATED:
89-
actions = [DiffAction.ADDED, DiffAction.REMOVED]
90-
actions_and_values: list[tuple[DiffAction, str]] = []
91-
for action in actions:
92-
if action is DiffAction.ADDED:
93-
actions_and_values.append((action, new_value))
94-
elif action is DiffAction.REMOVED and property_diff.previous_value:
95-
actions_and_values.append((action, property_diff.previous_value))
96-
97-
for action, value in actions_and_values:
98-
relationship_dicts.append(
99-
RelationshipMergeDict(
100-
peer_id=value, name=relationship_identifier, action=self._to_action_str(action=action)
101-
)
195+
# TODO: next PR will add properties to this method
196+
actions_and_values = self._get_property_actions_and_values(
197+
property_diff=property_diff, python_value_type=str
102198
)
103-
return relationship_dicts
199+
200+
for action, value in actions_and_values:
201+
relationship_dicts.append(
202+
RelationshipMergeDict(
203+
peer_id=str(value), name=relationship_identifier, action=self._to_action_str(action=action)
204+
)
205+
)
206+
return relationship_dicts, []

0 commit comments

Comments
 (0)