Skip to content

Commit 9ff2ff8

Browse files
authored
fix hierarchical relationship handling in diff (#4889)
1 parent b45c324 commit 9ff2ff8

File tree

3 files changed

+187
-4
lines changed

3 files changed

+187
-4
lines changed

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,34 @@ def peer_kind(self) -> Optional[str]:
706706
return None
707707
return str(self.property_node.get("kind"))
708708

709+
@property
710+
def possible_relationship_directions(self) -> list[RelationshipDirection]:
711+
path_to_node = "Node" in self.property_node.labels
712+
attr_start_node = self.path_to_attribute.start_node
713+
attr_end_node = self.path_to_attribute.end_node
714+
prop_start_node = self.path_to_property.start_node
715+
prop_end_node = self.path_to_property.end_node
716+
if path_to_node and (
717+
attr_start_node
718+
and attr_start_node.element_id == self.node_node.element_id
719+
and prop_start_node
720+
and prop_start_node.element_id == self.attribute_node.element_id
721+
):
722+
return [RelationshipDirection.OUTBOUND]
723+
if path_to_node and (
724+
attr_end_node
725+
and attr_end_node.element_id == self.node_node.element_id
726+
and prop_end_node
727+
and prop_end_node.element_id == self.attribute_node.element_id
728+
):
729+
return [RelationshipDirection.INBOUND]
730+
# if we only have one Node->Relationship path, we cannot fully determine the relationship direction
731+
if attr_start_node and attr_start_node.element_id == self.node_node.element_id:
732+
return [RelationshipDirection.OUTBOUND, RelationshipDirection.BIDIR]
733+
if attr_end_node and attr_end_node.element_id == self.node_node.element_id:
734+
return [RelationshipDirection.INBOUND, RelationshipDirection.BIDIR]
735+
return [RelationshipDirection.BIDIR, RelationshipDirection.INBOUND, RelationshipDirection.OUTBOUND]
736+
709737

710738
@dataclass
711739
class EnrichedNodeCreateRequest:

backend/infrahub/core/diff/query_parser.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
if TYPE_CHECKING:
2323
from infrahub.core.branch import Branch
2424
from infrahub.core.query import QueryResult
25+
from infrahub.core.schema import MainSchemaTypes
2526
from infrahub.core.schema.manager import SchemaManager
2627
from infrahub.core.schema.relationship_schema import RelationshipSchema
2728

@@ -517,6 +518,18 @@ def _get_diff_node(self, database_path: DatabasePath, diff_root: DiffRootInterme
517518
diff_node.track_database_path(database_path=database_path)
518519
return diff_node
519520

521+
def _get_relationship_schema(
522+
self, database_path: DatabasePath, node_schema: MainSchemaTypes
523+
) -> RelationshipSchema | None:
524+
relationship_schemas = node_schema.get_relationships_by_identifier(id=database_path.attribute_name)
525+
if len(relationship_schemas) == 1:
526+
return relationship_schemas[0]
527+
possible_path_directions = database_path.possible_relationship_directions
528+
for rel_schema in relationship_schemas:
529+
if rel_schema.direction in possible_path_directions:
530+
return rel_schema
531+
return None
532+
520533
def _update_attribute_level(self, database_path: DatabasePath, diff_node: DiffNodeIntermediate) -> None:
521534
node_schema = self.schema_manager.get(
522535
name=database_path.node_kind, branch=database_path.deepest_branch, duplicate=False
@@ -525,9 +538,7 @@ def _update_attribute_level(self, database_path: DatabasePath, diff_node: DiffNo
525538
diff_attribute = self._get_diff_attribute(database_path=database_path, diff_node=diff_node)
526539
self._update_attribute_property(database_path=database_path, diff_attribute=diff_attribute)
527540
return
528-
relationship_schema = node_schema.get_relationship_by_identifier(
529-
id=database_path.attribute_name, raise_on_error=False
530-
)
541+
relationship_schema = self._get_relationship_schema(database_path=database_path, node_schema=node_schema)
531542
if not relationship_schema:
532543
return
533544
diff_relationship = self._get_diff_relationship(diff_node=diff_node, relationship_schema=relationship_schema)

backend/tests/unit/core/diff/test_diff_calculator.py

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import pytest
22

3+
from infrahub.core import registry
34
from infrahub.core.branch import Branch
4-
from infrahub.core.constants import DiffAction, RelationshipCardinality
5+
from infrahub.core.constants import DiffAction, InfrahubKind, RelationshipCardinality
56
from infrahub.core.constants.database import DatabaseEdgeType
67
from infrahub.core.diff.calculator import DiffCalculator
78
from infrahub.core.diff.model.path import NodeFieldSpecifier
@@ -2283,3 +2284,146 @@ async def test_property_update_then_relationship_deleted(
22832284

22842285
base_diff_root = calculated_diffs.base_branch_diff
22852286
assert base_diff_root.nodes == []
2287+
2288+
2289+
async def test_hierarchy_with_same_kind_parent_and_child(
2290+
db: InfrahubDatabase,
2291+
default_branch: Branch,
2292+
register_core_models_schema: SchemaBranch,
2293+
register_ipam_schema: SchemaBranch,
2294+
):
2295+
prefix_schema = registry.schema.get_node_schema(name="IpamIPPrefix")
2296+
ip_namespace = await Node.init(db=db, schema=InfrahubKind.NAMESPACE)
2297+
await ip_namespace.new(db=db, name="ns1")
2298+
await ip_namespace.save(db=db)
2299+
top_node = await Node.init(db=db, schema=prefix_schema)
2300+
await top_node.new(db=db, prefix="10.0.0.0/7", ip_namespace=ip_namespace)
2301+
await top_node.save(db=db)
2302+
mid_node = await Node.init(db=db, schema=prefix_schema)
2303+
await mid_node.new(db=db, prefix="10.0.0.0/8", ip_namespace=ip_namespace)
2304+
await mid_node.save(db=db)
2305+
bottom_node = await Node.init(db=db, schema=prefix_schema)
2306+
await bottom_node.new(db=db, prefix="10.0.0.0/9", ip_namespace=ip_namespace)
2307+
await bottom_node.save(db=db)
2308+
branch = await create_branch(db=db, branch_name="branch2")
2309+
from_time = Timestamp()
2310+
mid_branch = await NodeManager.get_one(db=db, branch=branch, id=mid_node.id)
2311+
await mid_branch.parent.update(db=db, data=top_node.id)
2312+
await mid_branch.save(db=db)
2313+
bottom_branch = await NodeManager.get_one(db=db, branch=branch, id=bottom_node.id)
2314+
await bottom_branch.parent.update(db=db, data=mid_node.id)
2315+
await bottom_branch.save(db=db)
2316+
2317+
diff_calculator = DiffCalculator(db=db)
2318+
calculated_diffs = await diff_calculator.calculate_diff(
2319+
base_branch=default_branch, diff_branch=branch, from_time=from_time, to_time=Timestamp()
2320+
)
2321+
2322+
branch_root_path = calculated_diffs.diff_branch_diff
2323+
assert branch_root_path.branch == branch.name
2324+
nodes_by_id = {n.uuid: n for n in branch_root_path.nodes}
2325+
assert set(nodes_by_id.keys()) == {top_node.id, mid_node.id, bottom_node.id}
2326+
# top node
2327+
node_diff = nodes_by_id[top_node.id]
2328+
assert node_diff.action is DiffAction.UPDATED
2329+
assert len(node_diff.attributes) == 0
2330+
rels_by_name = {r.name: r for r in node_diff.relationships}
2331+
assert set(rels_by_name.keys()) == {"children"}
2332+
children_rel = rels_by_name["children"]
2333+
assert children_rel.action is DiffAction.UPDATED
2334+
assert len(children_rel.relationships) == 1
2335+
child_element = children_rel.relationships[0]
2336+
assert child_element.action is DiffAction.ADDED
2337+
assert child_element.peer_id == mid_node.id
2338+
properties_by_type = {p.property_type: p for p in child_element.properties}
2339+
assert set(properties_by_type.keys()) == {
2340+
DatabaseEdgeType.IS_RELATED,
2341+
DatabaseEdgeType.IS_PROTECTED,
2342+
DatabaseEdgeType.IS_VISIBLE,
2343+
}
2344+
for prop_type, new_value in (
2345+
(DatabaseEdgeType.IS_RELATED, mid_node.id),
2346+
(DatabaseEdgeType.IS_PROTECTED, False),
2347+
(DatabaseEdgeType.IS_VISIBLE, True),
2348+
):
2349+
diff_prop = properties_by_type[prop_type]
2350+
assert diff_prop.action is DiffAction.ADDED
2351+
assert diff_prop.previous_value is None
2352+
assert diff_prop.new_value == new_value
2353+
2354+
# middle node
2355+
node_diff = nodes_by_id[mid_node.id]
2356+
assert node_diff.action is DiffAction.UPDATED
2357+
assert len(node_diff.attributes) == 0
2358+
rels_by_name = {r.name: r for r in node_diff.relationships}
2359+
assert set(rels_by_name.keys()) == {"parent", "children"}
2360+
parent_rel = rels_by_name["parent"]
2361+
assert parent_rel.action is DiffAction.ADDED
2362+
assert len(parent_rel.relationships) == 1
2363+
parent_element = parent_rel.relationships[0]
2364+
assert parent_element.action is DiffAction.ADDED
2365+
assert parent_element.peer_id == top_node.id
2366+
properties_by_type = {p.property_type: p for p in parent_element.properties}
2367+
assert set(properties_by_type.keys()) == {
2368+
DatabaseEdgeType.IS_RELATED,
2369+
DatabaseEdgeType.IS_PROTECTED,
2370+
DatabaseEdgeType.IS_VISIBLE,
2371+
}
2372+
for prop_type, new_value in (
2373+
(DatabaseEdgeType.IS_RELATED, top_node.id),
2374+
(DatabaseEdgeType.IS_PROTECTED, False),
2375+
(DatabaseEdgeType.IS_VISIBLE, True),
2376+
):
2377+
diff_prop = properties_by_type[prop_type]
2378+
assert diff_prop.action is DiffAction.ADDED
2379+
assert diff_prop.previous_value is None
2380+
assert diff_prop.new_value == new_value
2381+
children_rel = rels_by_name["children"]
2382+
assert children_rel.action is DiffAction.UPDATED
2383+
assert len(children_rel.relationships) == 1
2384+
child_element = children_rel.relationships[0]
2385+
assert child_element.action is DiffAction.ADDED
2386+
assert child_element.peer_id == bottom_node.id
2387+
properties_by_type = {p.property_type: p for p in child_element.properties}
2388+
assert set(properties_by_type.keys()) == {
2389+
DatabaseEdgeType.IS_RELATED,
2390+
DatabaseEdgeType.IS_PROTECTED,
2391+
DatabaseEdgeType.IS_VISIBLE,
2392+
}
2393+
for prop_type, new_value in (
2394+
(DatabaseEdgeType.IS_RELATED, bottom_node.id),
2395+
(DatabaseEdgeType.IS_PROTECTED, False),
2396+
(DatabaseEdgeType.IS_VISIBLE, True),
2397+
):
2398+
diff_prop = properties_by_type[prop_type]
2399+
assert diff_prop.action is DiffAction.ADDED
2400+
assert diff_prop.previous_value is None
2401+
assert diff_prop.new_value == new_value
2402+
2403+
# bottom node
2404+
node_diff = nodes_by_id[bottom_node.id]
2405+
assert node_diff.action is DiffAction.UPDATED
2406+
assert len(node_diff.attributes) == 0
2407+
rels_by_name = {r.name: r for r in node_diff.relationships}
2408+
assert set(rels_by_name.keys()) == {"parent"}
2409+
parent_rel = rels_by_name["parent"]
2410+
assert parent_rel.action is DiffAction.ADDED
2411+
assert len(parent_rel.relationships) == 1
2412+
parent_element = parent_rel.relationships[0]
2413+
assert parent_element.action is DiffAction.ADDED
2414+
assert parent_element.peer_id == mid_node.id
2415+
properties_by_type = {p.property_type: p for p in parent_element.properties}
2416+
assert set(properties_by_type.keys()) == {
2417+
DatabaseEdgeType.IS_RELATED,
2418+
DatabaseEdgeType.IS_PROTECTED,
2419+
DatabaseEdgeType.IS_VISIBLE,
2420+
}
2421+
for prop_type, new_value in (
2422+
(DatabaseEdgeType.IS_RELATED, mid_node.id),
2423+
(DatabaseEdgeType.IS_PROTECTED, False),
2424+
(DatabaseEdgeType.IS_VISIBLE, True),
2425+
):
2426+
diff_prop = properties_by_type[prop_type]
2427+
assert diff_prop.action is DiffAction.ADDED
2428+
assert diff_prop.previous_value is None
2429+
assert diff_prop.new_value == new_value

0 commit comments

Comments
 (0)