Skip to content

Commit 74fd1b4

Browse files
authored
add new WIP DiffMerger class (#4505)
* IFC-675 start new DiffMerger class * add support for node conflicts * neo4j WITH fix
1 parent eb1f86a commit 74fd1b4

File tree

6 files changed

+394
-0
lines changed

6 files changed

+394
-0
lines changed

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

Whitespace-only changes.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
from infrahub.core.diff.model.path import BranchTrackingId
6+
from infrahub.core.diff.query.merge import DiffMergeQuery
7+
8+
if TYPE_CHECKING:
9+
from infrahub.core.branch import Branch
10+
from infrahub.core.diff.repository.repository import DiffRepository
11+
from infrahub.core.timestamp import Timestamp
12+
from infrahub.database import InfrahubDatabase
13+
14+
from .serializer import DiffMergeSerializer
15+
16+
17+
class DiffMerger:
18+
def __init__(
19+
self,
20+
db: InfrahubDatabase,
21+
source_branch: Branch,
22+
destination_branch: Branch,
23+
diff_repository: DiffRepository,
24+
serializer: DiffMergeSerializer,
25+
):
26+
self.source_branch = source_branch
27+
self.destination_branch = destination_branch
28+
self.db = db
29+
self.diff_repository = diff_repository
30+
self.serializer = serializer
31+
32+
async def merge_graph(self, at: Timestamp) -> None:
33+
enriched_diff = await self.diff_repository.get_one(
34+
diff_branch_name=self.source_branch.name, tracking_id=BranchTrackingId(name=self.source_branch.name)
35+
)
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)
45+
46+
self.source_branch.branched_from = at.to_string()
47+
await self.source_branch.save(db=self.db)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from typing import Any
2+
3+
from infrahub.core.constants import DiffAction
4+
5+
from ..model.path import ConflictSelection, EnrichedDiffConflict, EnrichedDiffRoot
6+
7+
8+
class DiffMergeSerializer:
9+
def _get_action(self, action: DiffAction, conflict: EnrichedDiffConflict | None) -> DiffAction:
10+
if not conflict:
11+
return action
12+
if conflict.selected_branch is ConflictSelection.BASE_BRANCH:
13+
return conflict.base_branch_action
14+
if conflict.selected_branch is ConflictSelection.DIFF_BRANCH:
15+
return conflict.diff_branch_action
16+
raise ValueError(f"conflict {conflict.uuid} does not have a branch selection")
17+
18+
async def serialize(self, diff: EnrichedDiffRoot) -> list[dict[str, Any]]:
19+
serialized_node_diffs = []
20+
for node in diff.nodes:
21+
node_action = self._get_action(action=node.action, conflict=node.conflict)
22+
serialized_node_diffs.append(
23+
{
24+
"action": str(node_action.value).upper(),
25+
"uuid": node.uuid,
26+
}
27+
)
28+
return serialized_node_diffs
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Any
4+
5+
from infrahub.core.query import Query, QueryType
6+
7+
if TYPE_CHECKING:
8+
from infrahub.core.branch import Branch
9+
from infrahub.core.timestamp import Timestamp
10+
from infrahub.database import InfrahubDatabase
11+
12+
13+
class DiffMergeQuery(Query):
14+
name = "diff_merge"
15+
type = QueryType.WRITE
16+
insert_return = False
17+
18+
def __init__(
19+
self,
20+
node_diff_dicts: dict[str, Any],
21+
at: Timestamp,
22+
target_branch: Branch,
23+
**kwargs: Any,
24+
) -> None:
25+
super().__init__(**kwargs)
26+
self.node_diff_dicts = node_diff_dicts
27+
self.at = at
28+
self.target_branch = target_branch
29+
self.source_branch_name = self.branch.name
30+
31+
async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None:
32+
self.params = {
33+
"node_diff_dicts": self.node_diff_dicts,
34+
"at": self.at.to_string(),
35+
"branch_level": self.target_branch.hierarchy_level,
36+
"target_branch": self.target_branch.name,
37+
"source_branch": self.source_branch_name,
38+
}
39+
query = """
40+
UNWIND $node_diff_dicts AS node_diff_map
41+
CALL {
42+
WITH node_diff_map
43+
WITH node_diff_map, CASE
44+
WHEN node_diff_map.action = "ADDED" THEN "active"
45+
WHEN node_diff_map.action = "REMOVED" THEN "deleted"
46+
ELSE NULL
47+
END AS node_rel_status
48+
CALL {
49+
// ------------------------------
50+
// only make IS_PART_OF updates if node is ADDED or REMOVED
51+
// ------------------------------
52+
WITH node_diff_map, node_rel_status
53+
WITH node_diff_map, node_rel_status
54+
WHERE node_rel_status IS NOT NULL
55+
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+
}
68+
// ------------------------------
69+
// set IS_PART_OF.to on source branch and, optionally, target branch
70+
// ------------------------------
71+
WITH root, r_root, n, node_rel_status
72+
CALL {
73+
WITH root, n, node_rel_status
74+
OPTIONAL MATCH (root)<-[source_r_root:IS_PART_OF {branch: $source_branch, status: node_rel_status}]-(n)
75+
WHERE source_r_root.from <= $at AND source_r_root.to IS NULL
76+
SET source_r_root.to = $at
77+
}
78+
WITH root, r_root, n, node_rel_status
79+
CALL {
80+
WITH root, n, node_rel_status
81+
OPTIONAL MATCH (root)<-[target_r_root:IS_PART_OF {branch: $target_branch, status: "active"}]-(n)
82+
WHERE node_rel_status = "deleted"
83+
AND target_r_root.from <= $at AND target_r_root.to IS NULL
84+
SET target_r_root.to = $at
85+
}
86+
// ------------------------------
87+
// create new IS_PART_OF relationship on target_branch
88+
// ------------------------------
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)
92+
}
93+
}
94+
"""
95+
self.add_to_query(query=query)

backend/infrahub/core/node/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ def get_id(self) -> str:
6666

6767
raise InitializationError("The node has not been saved yet and doesn't have an id")
6868

69+
def get_updated_at(self) -> Timestamp | None:
70+
return self._updated_at
71+
6972
async def get_hfid(self, db: InfrahubDatabase, include_kind: bool = False) -> Optional[list[str]]:
7073
"""Return the Human friendly id of the node."""
7174
if not self._schema.human_friendly_id:

0 commit comments

Comments
 (0)