Skip to content

Commit b503d59

Browse files
authored
update WIP diff merge query to handle relationships and attributes (#4566)
* update diff merge query to handle relationships and attributes * neo4j syntax fix * maintain directionality for relationships
1 parent a6ed25c commit b503d59

File tree

4 files changed

+346
-34
lines changed

4 files changed

+346
-34
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from typing_extensions import TypedDict
2+
3+
4+
class RelationshipMergeDict(TypedDict):
5+
peer_id: str
6+
name: str
7+
action: str
8+
9+
10+
class AttributeMergeDict(TypedDict):
11+
name: str
12+
action: str
13+
14+
15+
class NodeMergeDict(TypedDict):
16+
uuid: str
17+
action: str
18+
attributes: list[AttributeMergeDict]
19+
relationships: list[RelationshipMergeDict]
Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
1-
from typing import Any
2-
31
from infrahub.core.constants import DiffAction
2+
from infrahub.core.constants.database import DatabaseEdgeType
3+
from infrahub.core.schema.schema_branch import SchemaBranch
44

5-
from ..model.path import ConflictSelection, EnrichedDiffConflict, EnrichedDiffRoot
5+
from ..model.path import (
6+
ConflictSelection,
7+
EnrichedDiffAttribute,
8+
EnrichedDiffConflict,
9+
EnrichedDiffRoot,
10+
EnrichedDiffSingleRelationship,
11+
)
12+
from .model import AttributeMergeDict, NodeMergeDict, RelationshipMergeDict
613

714

815
class DiffMergeSerializer:
16+
def __init__(self, schema_branch: SchemaBranch) -> None:
17+
self.schema_branch = schema_branch
18+
self._relationship_id_cache: dict[tuple[str, str], str] = {}
19+
920
def _get_action(self, action: DiffAction, conflict: EnrichedDiffConflict | None) -> DiffAction:
1021
if not conflict:
1122
return action
@@ -15,14 +26,78 @@ def _get_action(self, action: DiffAction, conflict: EnrichedDiffConflict | None)
1526
return conflict.diff_branch_action
1627
raise ValueError(f"conflict {conflict.uuid} does not have a branch selection")
1728

18-
async def serialize(self, diff: EnrichedDiffRoot) -> list[dict[str, Any]]:
29+
def _to_action_str(self, action: DiffAction) -> str:
30+
return str(action.value).upper()
31+
32+
def _get_relationship_identifier(self, schema_kind_str: str, relationship_name: str) -> str:
33+
cache_key = (schema_kind_str, relationship_name)
34+
if cache_key in self._relationship_id_cache:
35+
return self._relationship_id_cache[cache_key]
36+
node_schema = self.schema_branch.get(name=schema_kind_str, duplicate=False)
37+
relationship_schema = node_schema.get_relationship(name=relationship_name)
38+
relationship_identifier = relationship_schema.get_identifier()
39+
self._relationship_id_cache[cache_key] = relationship_identifier
40+
return relationship_identifier
41+
42+
async def serialize(self, diff: EnrichedDiffRoot) -> list[NodeMergeDict]:
1943
serialized_node_diffs = []
2044
for node in diff.nodes:
2145
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]
47+
relationship_diffs = []
48+
for rel_diff in node.relationships:
49+
relationship_identifier = self._get_relationship_identifier(
50+
schema_kind_str=node.kind, relationship_name=rel_diff.name
51+
)
52+
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+
)
57+
)
2258
serialized_node_diffs.append(
23-
{
24-
"action": str(node_action.value).upper(),
25-
"uuid": node.uuid,
26-
}
59+
NodeMergeDict(
60+
uuid=node.uuid,
61+
action=self._to_action_str(action=node_action),
62+
attributes=attribute_diffs,
63+
relationships=relationship_diffs,
64+
)
2765
)
2866
return serialized_node_diffs
67+
68+
def _serialize_attribute(self, attribute_diff: EnrichedDiffAttribute) -> AttributeMergeDict:
69+
return AttributeMergeDict(
70+
name=attribute_diff.name,
71+
action=self._to_action_str(action=attribute_diff.action),
72+
)
73+
74+
def _serialize_relationship_element(
75+
self, relationship_diff: EnrichedDiffSingleRelationship, relationship_identifier: str
76+
) -> list[RelationshipMergeDict]:
77+
relationship_dicts = []
78+
for property_diff in relationship_diff.properties:
79+
if property_diff.property_type is not DatabaseEdgeType.IS_RELATED:
80+
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+
)
102+
)
103+
return relationship_dicts

backend/infrahub/core/diff/query/merge.py

Lines changed: 187 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,13 @@ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None:
4040
UNWIND $node_diff_dicts AS node_diff_map
4141
CALL {
4242
WITH node_diff_map
43-
WITH node_diff_map, CASE
43+
MATCH (n:Node {uuid: node_diff_map.uuid})
44+
RETURN n
45+
}
46+
WITH n, node_diff_map
47+
CALL {
48+
WITH n, node_diff_map
49+
WITH n, node_diff_map, CASE
4450
WHEN node_diff_map.action = "ADDED" THEN "active"
4551
WHEN node_diff_map.action = "REMOVED" THEN "deleted"
4652
ELSE NULL
@@ -49,33 +55,21 @@ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None:
4955
// ------------------------------
5056
// only make IS_PART_OF updates if node is ADDED or REMOVED
5157
// ------------------------------
52-
WITH node_diff_map, node_rel_status
53-
WITH node_diff_map, node_rel_status
58+
WITH n, node_diff_map, node_rel_status
59+
WITH n, node_diff_map, node_rel_status
5460
WHERE node_rel_status IS NOT NULL
5561
MATCH (root:Root)
56-
MATCH (n:Node {uuid: node_diff_map.uuid})
57-
// ------------------------------
58-
// check if IS_PART_OF relationship with node_rel_status already exists on the target branch
59-
// ------------------------------
60-
CALL {
61-
WITH root, n, node_rel_status
62-
OPTIONAL MATCH (root)<-[r_root:IS_PART_OF {branch: $target_branch}]-(n)
63-
WHERE r_root.status = node_rel_status
64-
AND r_root.from <= $at
65-
AND (r_root.to >= $at OR r_root.to IS NULL)
66-
RETURN r_root
67-
}
6862
// ------------------------------
6963
// set IS_PART_OF.to on source branch and, optionally, target branch
7064
// ------------------------------
71-
WITH root, r_root, n, node_rel_status
65+
WITH root, n, node_rel_status
7266
CALL {
7367
WITH root, n, node_rel_status
7468
OPTIONAL MATCH (root)<-[source_r_root:IS_PART_OF {branch: $source_branch, status: node_rel_status}]-(n)
7569
WHERE source_r_root.from <= $at AND source_r_root.to IS NULL
7670
SET source_r_root.to = $at
7771
}
78-
WITH root, r_root, n, node_rel_status
72+
WITH root, n, node_rel_status
7973
CALL {
8074
WITH root, n, node_rel_status
8175
OPTIONAL MATCH (root)<-[target_r_root:IS_PART_OF {branch: $target_branch, status: "active"}]-(n)
@@ -86,10 +80,183 @@ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None:
8680
// ------------------------------
8781
// create new IS_PART_OF relationship on target_branch
8882
// ------------------------------
89-
WITH root, r_root, n, node_rel_status
90-
WHERE r_root IS NULL
91-
CREATE (root)<-[:IS_PART_OF { branch: $target_branch, branch_level: $branch_level, from: $at, status: node_rel_status }]-(n)
83+
WITH root, n, node_rel_status
84+
CALL {
85+
WITH root, n, node_rel_status
86+
OPTIONAL MATCH (root)<-[r_root:IS_PART_OF {branch: $target_branch}]-(n)
87+
WHERE r_root.status = node_rel_status
88+
AND r_root.from <= $at
89+
AND (r_root.to >= $at OR r_root.to IS NULL)
90+
WITH root, r_root, n, node_rel_status
91+
WHERE r_root IS NULL
92+
CREATE (root)
93+
<-[:IS_PART_OF { branch: $target_branch, branch_level: $branch_level, from: $at, status: node_rel_status }]
94+
-(n)
95+
}
96+
}
97+
WITH n, node_diff_map
98+
CALL {
99+
WITH n, node_diff_map
100+
UNWIND node_diff_map.attributes AS attribute_diff_map
101+
// ------------------------------
102+
// handle updates for attributes under this node
103+
// ------------------------------
104+
CALL {
105+
WITH n, attribute_diff_map
106+
WITH n, attribute_diff_map.name AS attr_name, CASE
107+
WHEN attribute_diff_map.action = "ADDED" THEN "active"
108+
WHEN attribute_diff_map.action = "REMOVED" THEN "deleted"
109+
ELSE NULL
110+
END AS attr_rel_status
111+
CALL {
112+
WITH n, attr_name
113+
MATCH (n)-[:HAS_ATTRIBUTE]->(a:Attribute {name: attr_name})
114+
RETURN a
115+
LIMIT 1
116+
}
117+
WITH n, attr_rel_status, a
118+
// ------------------------------
119+
// set HAS_ATTRIBUTE.to on source branch if necessary
120+
// ------------------------------
121+
CALL {
122+
WITH n, attr_rel_status, a
123+
OPTIONAL MATCH (n)
124+
-[source_r_attr:HAS_ATTRIBUTE {branch: $source_branch, status: attr_rel_status}]
125+
->(a)
126+
WHERE source_r_attr.from <= $at AND source_r_attr.to IS NULL
127+
SET source_r_attr.to = $at
128+
}
129+
WITH n, attr_rel_status, a
130+
// ------------------------------
131+
// conditionally create new HAS_ATTRIBUTE relationship on target_branch, if necessary
132+
// ------------------------------
133+
CALL {
134+
WITH n, attr_rel_status, a
135+
OPTIONAL MATCH (n)-[r_attr:HAS_ATTRIBUTE {branch: $target_branch}]->(a)
136+
WHERE r_attr.status = attr_rel_status
137+
AND r_attr.from <= $at
138+
AND (r_attr.to >= $at OR r_attr.to IS NULL)
139+
WITH n, r_attr, attr_rel_status, a
140+
WHERE r_attr IS NULL
141+
CREATE (n)-[:HAS_ATTRIBUTE { branch: $target_branch, branch_level: $branch_level, from: $at, status: attr_rel_status }]->(a)
142+
}
143+
RETURN a
144+
}
145+
RETURN a
146+
}
147+
WITH n, node_diff_map
148+
CALL {
149+
WITH n,node_diff_map
150+
UNWIND node_diff_map.relationships AS relationship_diff_map
151+
// ------------------------------
152+
// handle updates for relationships under this node
153+
// ------------------------------
154+
CALL {
155+
WITH n, relationship_diff_map
156+
WITH n, relationship_diff_map.peer_id AS rel_peer_id, relationship_diff_map.name AS rel_name, CASE
157+
WHEN relationship_diff_map.action = "ADDED" THEN "active"
158+
WHEN relationship_diff_map.action = "REMOVED" THEN "deleted"
159+
ELSE NULL
160+
END AS related_rel_status
161+
// ------------------------------
162+
// set IS_RELATED.to on source branch and, optionally, target_branch
163+
// ------------------------------
164+
CALL {
165+
WITH n, rel_name, rel_peer_id, related_rel_status
166+
MATCH (n)
167+
-[source_r_rel_1:IS_RELATED {branch: $source_branch, status: related_rel_status}]
168+
-(r:Relationship {name: rel_name})
169+
-[source_r_rel_2:IS_RELATED {branch: $source_branch, status: related_rel_status}]
170+
-(:Node {uuid: rel_peer_id})
171+
WHERE source_r_rel_1.from <= $at AND source_r_rel_1.to IS NULL
172+
AND source_r_rel_2.from <= $at AND source_r_rel_2.to IS NULL
173+
SET source_r_rel_1.to = $at
174+
SET source_r_rel_2.to = $at
175+
176+
// ------------------------------
177+
// determine the directions of each IS_RELATED
178+
// ------------------------------
179+
RETURN r, CASE
180+
WHEN startNode(source_r_rel_1).uuid = n.uuid THEN "r"
181+
ELSE "l"
182+
END AS r1_dir,
183+
CASE
184+
WHEN startNode(source_r_rel_2).uuid = r.uuid THEN "r"
185+
ELSE "l"
186+
END AS r2_dir
187+
}
188+
WITH n, r, r1_dir, r2_dir, rel_name, rel_peer_id, related_rel_status
189+
CALL {
190+
WITH n, rel_name, rel_peer_id, related_rel_status
191+
OPTIONAL MATCH (n)
192+
-[target_r_rel_1:IS_RELATED {branch: $target_branch, status: "active"}]
193+
-(:Relationship {name: rel_name})
194+
-[target_r_rel_2:IS_RELATED {branch: $target_branch, status: "active"}]
195+
-(:Node {uuid: rel_peer_id})
196+
WHERE related_rel_status = "deleted"
197+
AND target_r_rel_1.from <= $at AND target_r_rel_1.to IS NULL
198+
AND target_r_rel_2.from <= $at AND target_r_rel_2.to IS NULL
199+
SET target_r_rel_1.to = $at
200+
SET target_r_rel_2.to = $at
201+
}
202+
WITH n, r, r1_dir, r2_dir, rel_name, rel_peer_id, related_rel_status
203+
// ------------------------------
204+
// conditionally create new IS_RELATED relationships on target_branch, if necessary
205+
// ------------------------------
206+
CALL {
207+
WITH n, r, r1_dir, r2_dir, rel_name, rel_peer_id, related_rel_status
208+
MATCH (p:Node {uuid: rel_peer_id})
209+
OPTIONAL MATCH (n)
210+
-[r_rel_1:IS_RELATED {branch: $target_branch, status: related_rel_status}]
211+
-(:Relationship {name: rel_name})
212+
-[r_rel_2:IS_RELATED {branch: $target_branch, status: related_rel_status}]
213+
-(p)
214+
WHERE r_rel_1.from <= $at
215+
AND (r_rel_1.to >= $at OR r_rel_1.to IS NULL)
216+
AND r_rel_2.from <= $at
217+
AND (r_rel_2.to >= $at OR r_rel_2.to IS NULL)
218+
WITH n, r, r1_dir, r2_dir, p, related_rel_status, r_rel_1, r_rel_2
219+
WHERE r_rel_1 IS NULL
220+
AND r_rel_2 IS NULL
221+
// ------------------------------
222+
// create IS_RELATED relationships with directions maintained from source
223+
// ------------------------------
224+
CALL {
225+
WITH n, r, r1_dir, related_rel_status
226+
WITH n, r, r1_dir, related_rel_status
227+
WHERE r1_dir = "r"
228+
CREATE (n)
229+
-[:IS_RELATED {branch: $target_branch, branch_level: $branch_level, from: $at, status: related_rel_status}]
230+
->(r)
231+
}
232+
CALL {
233+
WITH n, r, r1_dir, related_rel_status
234+
WITH n, r, r1_dir, related_rel_status
235+
WHERE r1_dir = "l"
236+
CREATE (n)
237+
<-[:IS_RELATED {branch: $target_branch, branch_level: $branch_level, from: $at, status: related_rel_status}]
238+
-(r)
239+
}
240+
CALL {
241+
WITH r, p, r2_dir, related_rel_status
242+
WITH r, p, r2_dir, related_rel_status
243+
WHERE r2_dir = "r"
244+
CREATE (r)
245+
-[:IS_RELATED {branch: $target_branch, branch_level: $branch_level, from: $at, status: related_rel_status}]
246+
->(p)
247+
}
248+
CALL {
249+
WITH r, p, r2_dir, related_rel_status
250+
WITH r, p, r2_dir, related_rel_status
251+
WHERE r2_dir = "l"
252+
CREATE (r)
253+
<-[:IS_RELATED {branch: $target_branch, branch_level: $branch_level, from: $at, status: related_rel_status}]
254+
-(p)
255+
}
256+
}
257+
}
92258
}
93259
}
260+
RETURN NULL AS done
94261
"""
95262
self.add_to_query(query=query)

0 commit comments

Comments
 (0)