Skip to content

Commit c5a0c81

Browse files
committed
Fix relationship changelogs with new RelationshipDeleteAllQuery
1 parent 45da0bf commit c5a0c81

File tree

8 files changed

+143
-57
lines changed

8 files changed

+143
-57
lines changed

backend/infrahub/core/changelog/diff.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def _process_node_cardinality_one_relationship(
177177
value_previous=rel_prop.previous_value,
178178
)
179179

180-
node.add_relationship(relationship=changelog_rel)
180+
node.add_relationship(relationship_changelog=changelog_rel)
181181

182182
def _convert_string_boolean_value(self, value: str | None) -> bool | None:
183183
"""Convert string based boolean for is_protected and is_visible."""
@@ -227,7 +227,7 @@ def _process_node_cardinality_many_relationship(
227227

228228
changelog_rel.peers.append(peer_log)
229229

230-
node.add_relationship(relationship=changelog_rel)
230+
node.add_relationship(relationship_changelog=changelog_rel)
231231

232232
def collect_changelogs(self) -> Sequence[tuple[DiffAction, NodeChangelog]]:
233233
self._populate_diff_nodes()

backend/infrahub/core/changelog/models.py

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ def add_property(self, name: str, value_current: bool | str | None, value_previo
129129
def set_parent(self, parent_id: str, parent_kind: str) -> None:
130130
self._parent = ChangelogRelatedNode(node_id=parent_id, node_kind=parent_kind)
131131

132-
def set_parent_from_relationship(self, relationship: Relationship) -> None:
133-
if relationship.schema.kind == RelationshipKind.PARENT:
132+
def set_parent_from_relationship(self, rel_kind: RelationshipKind) -> None:
133+
if rel_kind == RelationshipKind.PARENT:
134134
if (
135135
self.peer_status in [DiffAction.ADDED, DiffAction.UNCHANGED, DiffAction.UPDATED]
136136
and self.peer_id
@@ -308,14 +308,14 @@ def add_attribute(self, attribute: AttributeChangelog) -> None:
308308
self.attributes[attribute.name] = attribute
309309

310310
def add_relationship(
311-
self, relationship: RelationshipCardinalityOneChangelog | RelationshipCardinalityManyChangelog
311+
self, relationship_changelog: RelationshipCardinalityOneChangelog | RelationshipCardinalityManyChangelog
312312
) -> None:
313-
if isinstance(relationship, RelationshipCardinalityOneChangelog) and relationship.parent:
314-
self.add_parent(parent=relationship.parent)
315-
if relationship.is_empty:
313+
if isinstance(relationship_changelog, RelationshipCardinalityOneChangelog) and relationship_changelog.parent:
314+
self.add_parent(parent=relationship_changelog.parent)
315+
if relationship_changelog.is_empty:
316316
return
317317

318-
self.relationships[relationship.name] = relationship
318+
self.relationships[relationship_changelog.name] = relationship_changelog
319319

320320
def create_attribute(self, attribute: BaseAttribute) -> None:
321321
changelog_attribute = AttributeChangelog(
@@ -383,7 +383,7 @@ def remove_peer(self, peer_data: RelationshipPeerData) -> None:
383383
def _set_cardinality_one_peer(self, relationship: Relationship) -> None:
384384
self.cardinality_one_relationship.peer_id = relationship.peer_id
385385
self.cardinality_one_relationship.peer_kind = relationship.get_peer_kind()
386-
self.cardinality_one_relationship.set_parent_from_relationship(relationship=relationship)
386+
self.cardinality_one_relationship.set_parent_from_relationship(rel_kind=relationship.schema.kind)
387387

388388
def add_parent_from_relationship(self, relationship: Relationship) -> None:
389389
if self.schema.cardinality == RelationshipCardinality.ONE:
@@ -420,18 +420,16 @@ def add_updated_relationship(
420420
value_current=getattr(relationship, property_name),
421421
value_previous=previous_value,
422422
)
423-
self.cardinality_one_relationship.set_parent_from_relationship(relationship=relationship)
423+
self.cardinality_one_relationship.set_parent_from_relationship(rel_kind=relationship.schema.kind)
424424

425-
def delete_relationship(self, relationship: Relationship) -> None:
425+
def delete_relationship(self, peer_id: str, peer_kind: str, rel_schema: RelationshipSchema) -> None:
426426
if self.schema.cardinality == RelationshipCardinality.ONE:
427-
self.cardinality_one_relationship.peer_id_previous = relationship.get_peer_id()
428-
self.cardinality_one_relationship.peer_kind_previous = relationship.get_peer_kind()
429-
self.cardinality_one_relationship.set_parent_from_relationship(relationship=relationship)
427+
self.cardinality_one_relationship.peer_id_previous = peer_id
428+
self.cardinality_one_relationship.peer_kind_previous = peer_kind
429+
self.cardinality_one_relationship.set_parent_from_relationship(rel_kind=rel_schema.kind)
430430

431431
elif self.schema.cardinality == RelationshipCardinality.MANY:
432-
self.cardinality_many_relationship.remove_peer(
433-
peer_id=relationship.get_peer_id(), peer_kind=relationship.get_peer_kind()
434-
)
432+
self.cardinality_many_relationship.remove_peer(peer_id=peer_id, peer_kind=peer_kind)
435433

436434
@property
437435
def changelog(self) -> RelationshipCardinalityOneChangelog | RelationshipCardinalityManyChangelog:

backend/infrahub/core/constants/schema.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from enum import Enum, Flag, auto
22

3+
PARENT_CHILD_IDENTIFIER = "parent__child"
4+
35

46
class FlagProperty(Enum):
57
IS_VISIBLE = "is_visible"

backend/infrahub/core/node/__init__.py

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,16 @@
77
from infrahub_sdk.uuidt import UUIDT
88

99
from infrahub.core import registry
10-
<<<<<<< HEAD
1110
from infrahub.core.changelog.models import NodeChangelog
1211
from infrahub.core.constants import (
12+
GLOBAL_BRANCH_NAME,
1313
OBJECT_TEMPLATE_NAME_ATTR,
1414
OBJECT_TEMPLATE_RELATIONSHIP_NAME,
15-
=======
16-
from infrahub.core.constants import (
17-
GLOBAL_BRANCH_NAME,
18-
>>>>>>> stable
1915
BranchSupportType,
2016
ComputedAttributeKind,
2117
InfrahubKind,
2218
RelationshipCardinality,
23-
<<<<<<< HEAD
2419
RelationshipKind,
25-
=======
26-
>>>>>>> stable
2720
)
2821
from infrahub.core.constants.schema import SchemaElementPathType
2922
from infrahub.core.protocols import CoreNumberPool, CoreObjectTemplate
@@ -659,7 +652,7 @@ async def _update(
659652
processed_relationships.append(name)
660653
rel: RelationshipManager = getattr(self, name)
661654
updated_relationship = await rel.save(at=update_at, db=db)
662-
node_changelog.add_relationship(relationship=updated_relationship)
655+
node_changelog.add_relationship(relationship_changelog=updated_relationship)
663656

664657
if len(processed_relationships) != len(self._relationships):
665658
# Analyze if the node has a parent and add it to the changelog if missing
@@ -699,24 +692,17 @@ async def delete(self, db: InfrahubDatabase, at: Optional[Timestamp] = None) ->
699692
if deleted_attribute:
700693
node_changelog.add_attribute(attribute=deleted_attribute)
701694

702-
<<<<<<< HEAD
703-
# Go over the list of relationships and update them one by one
704-
for name in self._relationships:
705-
rel: RelationshipManager = getattr(self, name)
706-
updated_relationship = await rel.delete(at=delete_at, db=db)
707-
node_changelog.add_relationship(relationship=updated_relationship)
708-
709-
# Need to check if there are some unidirectional relationship as well
710-
# For example, if we delete a tag, we must check the permissions and update all the relationships pointing at it
711-
=======
712-
>>>>>>> stable
713695
branch = self.get_branch_based_on_support_type()
714696

715697
delete_query = await RelationshipDeleteAllQuery.init(
716698
db=db, node_id=self.get_id(), branch=branch, at=delete_at, branch_agnostic=branch.name == GLOBAL_BRANCH_NAME
717699
)
718700
await delete_query.execute(db=db)
719701

702+
deleted_relationships_changelogs = delete_query.get_deleted_relationships_changelog(self._schema)
703+
for relationship_changelog in deleted_relationships_changelogs:
704+
node_changelog.add_relationship(relationship_changelog=relationship_changelog)
705+
720706
# Update the relationship to the branch itself
721707
query = await NodeGetListQuery.init(
722708
db=db,

backend/infrahub/core/query/relationship.py

Lines changed: 111 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@
77

88
from infrahub_sdk.uuidt import UUIDT
99

10+
from infrahub.core.changelog.models import (
11+
ChangelogRelationshipMapper,
12+
RelationshipCardinalityManyChangelog,
13+
RelationshipCardinalityOneChangelog,
14+
)
1015
from infrahub.core.constants import RelationshipDirection, RelationshipStatus
1116
from infrahub.core.constants.database import DatabaseEdgeType
1217
from infrahub.core.query import Query, QueryType
1318
from infrahub.core.query.subquery import build_subquery_filter, build_subquery_order
1419
from infrahub.core.timestamp import Timestamp
1520
from infrahub.core.utils import extract_field_filters
21+
from infrahub.log import get_logger
1622

1723
if TYPE_CHECKING:
1824
from uuid import UUID
@@ -22,11 +28,13 @@
2228
from infrahub.core.branch import Branch
2329
from infrahub.core.node import Node
2430
from infrahub.core.relationship import Relationship
25-
from infrahub.core.schema import RelationshipSchema
31+
from infrahub.core.schema import NodeSchema, RelationshipSchema
2632
from infrahub.database import InfrahubDatabase
2733

2834
# pylint: disable=redefined-builtin,too-many-lines
2935

36+
log = get_logger()
37+
3038

3139
@dataclass
3240
class RelData:
@@ -959,6 +967,8 @@ class RelationshipDeleteAllQuery(Query):
959967
- Set `to` time if an active edge exist on the same branch.
960968
- Create `deleted` edge.
961969
- Apply above to every edges linked to any connected Relationship node.
970+
This query returns node uuids/kinds and corresponding relationship identifiers of deleted nodes,
971+
that are later used to update node changelog.
962972
"""
963973

964974
name = "node_delete_all_relationships"
@@ -969,7 +979,7 @@ def __init__(self, node_id: str, **kwargs):
969979
self.node_id = node_id
970980
super().__init__(**kwargs)
971981

972-
async def query_init(self, db: InfrahubDatabase, **kwargs) -> None:
982+
async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
973983
self.params["source_id"] = kwargs["node_id"]
974984
self.params["branch"] = self.branch.name
975985

@@ -987,30 +997,29 @@ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None:
987997
)
988998
self.params.update(rel_params)
989999

990-
query_match_relationships = """
1000+
query = """
9911001
MATCH (s:Node { uuid: $source_id })-[active_edge:IS_RELATED]-(rl:Relationship)
9921002
WHERE %(active_rel_filter)s AND active_edge.status = "active"
9931003
WITH DISTINCT rl
9941004
""" % {"active_rel_filter": active_rel_filter}
9951005

996-
self.add_to_query(query_match_relationships)
1006+
edge_types = [
1007+
DatabaseEdgeType.IS_VISIBLE.value,
1008+
DatabaseEdgeType.IS_PROTECTED.value,
1009+
DatabaseEdgeType.HAS_OWNER.value,
1010+
DatabaseEdgeType.HAS_SOURCE.value,
1011+
]
9971012

9981013
for arrow_left, arrow_right in (("<-", "-"), ("-", "->")):
999-
for edge_type in [
1000-
DatabaseEdgeType.IS_RELATED.value,
1001-
DatabaseEdgeType.IS_VISIBLE.value,
1002-
DatabaseEdgeType.IS_PROTECTED.value,
1003-
DatabaseEdgeType.HAS_OWNER.value,
1004-
DatabaseEdgeType.HAS_SOURCE.value,
1005-
]:
1006-
query = """
1014+
for edge_type in edge_types:
1015+
sub_query = """
10071016
CALL {
10081017
WITH rl
10091018
MATCH (rl)%(arrow_left)s[active_edge:%(edge_type)s]%(arrow_right)s(n)
10101019
WHERE %(active_rel_filter)s AND active_edge.status ="active"
10111020
CREATE (rl)%(arrow_left)s[deleted_edge:%(edge_type)s $rel_prop]%(arrow_right)s(n)
10121021
SET deleted_edge.hierarchy = active_edge.hierarchy
1013-
WITH active_edge
1022+
WITH active_edge, n
10141023
WHERE active_edge.branch = $branch AND active_edge.to IS NULL
10151024
SET active_edge.to = $at
10161025
}
@@ -1020,4 +1029,92 @@ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None:
10201029
"active_rel_filter": active_rel_filter,
10211030
"edge_type": edge_type,
10221031
}
1023-
self.add_to_query(query)
1032+
1033+
query += sub_query
1034+
1035+
# We only want to return uuid/kind of `Node` connected through `IS_RELATED` edges.
1036+
query += """
1037+
CALL {
1038+
WITH rl
1039+
MATCH (rl)-[active_edge:IS_RELATED]->(n)
1040+
WHERE %(active_rel_filter)s AND active_edge.status ="active"
1041+
CREATE (rl)-[deleted_edge:IS_RELATED $rel_prop]->(n)
1042+
SET deleted_edge.hierarchy = active_edge.hierarchy
1043+
WITH rl, active_edge, n
1044+
WHERE active_edge.branch = $branch AND active_edge.to IS NULL
1045+
SET active_edge.to = $at
1046+
RETURN
1047+
n.uuid as uuid,
1048+
n.kind as kind,
1049+
rl.name as rel_identifier,
1050+
"outbound" as rel_direction
1051+
1052+
UNION
1053+
1054+
WITH rl
1055+
MATCH (rl)<-[active_edge:IS_RELATED]-(n)
1056+
WHERE %(active_rel_filter)s AND active_edge.status ="active"
1057+
CREATE (rl)<-[deleted_edge:IS_RELATED $rel_prop]-(n)
1058+
SET deleted_edge.hierarchy = active_edge.hierarchy
1059+
WITH rl, active_edge, n
1060+
WHERE active_edge.branch = $branch AND active_edge.to IS NULL
1061+
SET active_edge.to = $at
1062+
RETURN
1063+
n.uuid as uuid,
1064+
n.kind as kind,
1065+
rl.name as rel_identifier,
1066+
"inbound" as rel_direction
1067+
}
1068+
RETURN DISTINCT uuid, kind, rel_identifier, rel_direction
1069+
""" % {
1070+
"active_rel_filter": active_rel_filter,
1071+
}
1072+
1073+
self.add_to_query(query)
1074+
1075+
def get_deleted_relationships_changelog(
1076+
self, node_schema: NodeSchema
1077+
) -> list[RelationshipCardinalityOneChangelog | RelationshipCardinalityManyChangelog]:
1078+
rel_identifier_to_changelog_mapper = {}
1079+
1080+
for result in self.get_results():
1081+
peer_uuid = result.data["uuid"]
1082+
if peer_uuid == self.node_id:
1083+
continue
1084+
1085+
rel_identifier = result.data["rel_identifier"]
1086+
kind = result.data["kind"]
1087+
deleted_rel_schemas = [
1088+
rel_schema for rel_schema in node_schema.relationships if rel_schema.identifier == rel_identifier
1089+
]
1090+
1091+
if len(deleted_rel_schemas) == 0:
1092+
continue # TODO Unidirectional relationship changelog should be handled, cf IFC-1319.
1093+
1094+
if len(deleted_rel_schemas) > 2:
1095+
log.error(f"Duplicated relationship schema with identifier {rel_identifier}")
1096+
continue
1097+
1098+
if len(deleted_rel_schemas) == 2:
1099+
# Hierarchical schema nodes have 2 relationships with `parent_child` identifiers,
1100+
# which are differentiated by their direction within the database.
1101+
# assert rel_identifier != PARENT_CHILD_IDENTIFIER
1102+
1103+
rel_direction = result.data["rel_direction"]
1104+
deleted_rel_schema = (
1105+
deleted_rel_schemas[0]
1106+
if deleted_rel_schemas[0].direction.value == rel_direction
1107+
else deleted_rel_schemas[1]
1108+
)
1109+
else:
1110+
deleted_rel_schema = deleted_rel_schemas[0]
1111+
1112+
try:
1113+
changelog_mapper = rel_identifier_to_changelog_mapper[rel_identifier]
1114+
except KeyError:
1115+
changelog_mapper = ChangelogRelationshipMapper(schema=deleted_rel_schema)
1116+
rel_identifier_to_changelog_mapper[rel_identifier] = changelog_mapper
1117+
1118+
changelog_mapper.delete_relationship(peer_id=peer_uuid, peer_kind=kind, rel_schema=deleted_rel_schema)
1119+
1120+
return [changelog_mapper.changelog for changelog_mapper in rel_identifier_to_changelog_mapper.values()]

backend/infrahub/core/relationship/model.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1208,7 +1208,9 @@ async def delete(
12081208
await self._fetch_relationships(at=delete_at, db=db, force_refresh=True)
12091209

12101210
for rel in await self.get_relationships(db=db):
1211-
relationship_mapper.delete_relationship(relationship=rel)
1211+
relationship_mapper.delete_relationship(
1212+
peer_kind=rel.get_peer_kind(), peer_id=rel.get_peer_id(), rel_schema=rel.schema
1213+
)
12121214
await rel.delete(at=delete_at, db=db)
12131215

12141216
return relationship_mapper.changelog

backend/infrahub/core/schema/schema_branch.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from infrahub.utils import format_label
5656
from infrahub.visuals import select_color
5757

58+
from ..constants.schema import PARENT_CHILD_IDENTIFIER
5859
from .constants import INTERNAL_SCHEMA_NODE_KINDS, SchemaNamespace
5960
from .schema_branch_computed import ComputedAttributes
6061

@@ -1535,7 +1536,7 @@ def add_groups(self) -> None:
15351536
def _get_hierarchy_child_rel(self, peer: str, hierarchical: str | None, read_only: bool) -> RelationshipSchema:
15361537
return RelationshipSchema(
15371538
name="children",
1538-
identifier="parent__child",
1539+
identifier=PARENT_CHILD_IDENTIFIER,
15391540
peer=peer,
15401541
kind=RelationshipKind.HIERARCHY,
15411542
cardinality=RelationshipCardinality.MANY,
@@ -1550,7 +1551,7 @@ def _get_hierarchy_parent_rel(
15501551
) -> RelationshipSchema:
15511552
return RelationshipSchema(
15521553
name="parent",
1553-
identifier="parent__child",
1554+
identifier=PARENT_CHILD_IDENTIFIER,
15541555
peer=peer,
15551556
kind=RelationshipKind.HIERARCHY,
15561557
cardinality=RelationshipCardinality.ONE,

backend/tests/unit/core/changelog/test_models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,8 @@ async def test_node_changelog_delete_with_cardinality_many_relationship(
299299
await person1_update.delete(db=db)
300300

301301
animals = person1_update.node_changelog.relationships["animals"].peers
302-
assert RelationshipPeerChangelog(peer_id=dog1.id, peer_kind="TestAnimal", peer_status=DiffAction.REMOVED) in animals
303-
assert RelationshipPeerChangelog(peer_id=dog2.id, peer_kind="TestAnimal", peer_status=DiffAction.REMOVED) in animals
302+
assert RelationshipPeerChangelog(peer_id=dog1.id, peer_kind="TestDog", peer_status=DiffAction.REMOVED) in animals
303+
assert RelationshipPeerChangelog(peer_id=dog2.id, peer_kind="TestDog", peer_status=DiffAction.REMOVED) in animals
304304

305305

306306
async def test_node_changelog_parent(db: InfrahubDatabase, default_branch, car_person_schema: None) -> None:

0 commit comments

Comments
 (0)