diff --git a/backend/infrahub/core/query/node.py b/backend/infrahub/core/query/node.py index cbd45438f4..ac96266c2e 100644 --- a/backend/infrahub/core/query/node.py +++ b/backend/infrahub/core/query/node.py @@ -413,9 +413,32 @@ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG self.params["branch"] = self.branch.name self.params["branch_level"] = self.branch.hierarchy_level + if self.branch.is_global or self.branch.is_default: + node_query_match = """ + MATCH (n:Node { uuid: $uuid }) + OPTIONAL MATCH (n)-[delete_edge:IS_PART_OF {status: "deleted", branch: $branch}]->(:Root) + WHERE delete_edge.from <= $at + WITH n WHERE delete_edge IS NULL + """ + else: + node_filter, node_filter_params = self.branch.get_query_filter_path(at=self.at, variable_name="r") + node_query_match = """ + MATCH (n:Node { uuid: $uuid }) + CALL { + WITH n + MATCH (n)-[r:IS_PART_OF]->(:Root) + WHERE %(node_filter)s + RETURN r.status = "active" AS is_active + ORDER BY r.from DESC + LIMIT 1 + } + WITH n WHERE is_active = TRUE + """ % {"node_filter": node_filter} + self.params.update(node_filter_params) + self.add_to_query(node_query_match) + query = """ MATCH (root:Root) - MATCH (n:Node { uuid: $uuid }) CREATE (n)-[r:IS_PART_OF { branch: $branch, branch_level: $branch_level, status: "deleted", from: $at }]->(root) """ diff --git a/backend/infrahub/core/query/relationship.py b/backend/infrahub/core/query/relationship.py index 2c8fd3ee3e..56ea01175f 100644 --- a/backend/infrahub/core/query/relationship.py +++ b/backend/infrahub/core/query/relationship.py @@ -205,50 +205,20 @@ def get_relationship_properties_dict(self, status: RelationshipStatus) -> dict[s rel_prop_dict["hierarchy"] = self.schema.hierarchical return rel_prop_dict - -class RelationshipCreateQuery(RelationshipQuery): - name = "relationship_create" - - type: QueryType = QueryType.WRITE - - def __init__( - self, - destination: Node = None, - destination_id: UUID | None = None, - **kwargs, - ): - if not destination and not destination_id: - raise ValueError("Either destination or destination_id must be provided.") - - super().__init__(destination=destination, destination_id=destination_id, **kwargs) - - async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002 - self.params["source_id"] = self.source_id - self.params["destination_id"] = self.destination_id - self.params["name"] = self.schema.identifier - self.params["branch_support"] = self.schema.branch.value - - self.params["uuid"] = str(UUIDT()) - - self.params["branch"] = self.branch.name - self.params["branch_level"] = self.branch.hierarchy_level - self.params["at"] = self.at.to_string() - - self.params["is_protected"] = self.rel.is_protected - self.params["is_visible"] = self.rel.is_visible - - source_branch = self.source.get_branch_based_on_support_type() + def add_source_match_to_query(self, source_branch: Branch) -> None: + self.params["source_id"] = self.source_id or self.source.get_id() if source_branch.is_global or source_branch.is_default: source_query_match = """ MATCH (s:Node { uuid: $source_id }) - WHERE NOT exists((s)-[:IS_PART_OF {status: "deleted", branch: $source_branch}]->(:Root)) + OPTIONAL MATCH (s)-[delete_edge:IS_PART_OF {status: "deleted", branch: $source_branch}]->(:Root) + WHERE delete_edge.from <= $at + WITH *, s WHERE delete_edge IS NULL """ self.params["source_branch"] = source_branch.name - else: - source_filter, source_filter_params = source_branch.get_query_filter_path( - at=self.at, variable_name="r", params_prefix="src_" - ) - source_query_match = """ + source_filter, source_filter_params = source_branch.get_query_filter_path( + at=self.at, variable_name="r", params_prefix="src_" + ) + source_query_match = """ MATCH (s:Node { uuid: $source_id }) CALL { WITH s @@ -258,16 +228,19 @@ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG ORDER BY r.from DESC LIMIT 1 } - WITH s WHERE s_is_active = TRUE + WITH *, s WHERE s_is_active = TRUE """ % {"source_filter": source_filter} - self.params.update(source_filter_params) + self.params.update(source_filter_params) self.add_to_query(source_query_match) - destination_branch = self.destination.get_branch_based_on_support_type() + def add_dest_match_to_query(self, destination_branch: Branch, destination_id: str) -> None: + self.params["destination_id"] = destination_id if destination_branch.is_global or destination_branch.is_default: destination_query_match = """ MATCH (d:Node { uuid: $destination_id }) - WHERE NOT exists((d)-[:IS_PART_OF {status: "deleted", branch: $destination_branch}]->(:Root)) + OPTIONAL MATCH (d)-[delete_edge:IS_PART_OF {status: "deleted", branch: $destination_branch}]->(:Root) + WHERE delete_edge.from <= $at + WITH *, d WHERE delete_edge IS NULL """ self.params["destination_branch"] = destination_branch.name else: @@ -284,11 +257,46 @@ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG ORDER BY r.from DESC LIMIT 1 } - WITH s, d WHERE d_is_active = TRUE + WITH *, d WHERE d_is_active = TRUE """ % {"destination_filter": destination_filter} self.params.update(destination_filter_params) self.add_to_query(destination_query_match) + +class RelationshipCreateQuery(RelationshipQuery): + name = "relationship_create" + + type: QueryType = QueryType.WRITE + + def __init__( + self, + destination: Node = None, + destination_id: UUID | None = None, + **kwargs, + ): + if not destination and not destination_id: + raise ValueError("Either destination or destination_id must be provided.") + + super().__init__(destination=destination, destination_id=destination_id, **kwargs) + + async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002 + self.params["name"] = self.schema.identifier + self.params["branch_support"] = self.schema.branch.value + + self.params["uuid"] = str(UUIDT()) + + self.params["branch"] = self.branch.name + self.params["branch_level"] = self.branch.hierarchy_level + self.params["at"] = self.at.to_string() + + self.params["is_protected"] = self.rel.is_protected + self.params["is_visible"] = self.rel.is_visible + + self.add_source_match_to_query(source_branch=self.source.get_branch_based_on_support_type()) + self.add_dest_match_to_query( + destination_branch=self.destination.get_branch_based_on_support_type(), + destination_id=self.destination_id or self.destination.get_id(), + ) self.query_add_all_node_property_match() self.params["rel_prop"] = self.get_relationship_properties_dict(status=RelationshipStatus.ACTIVE) @@ -433,7 +441,6 @@ def __init__( async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002 self.params["source_id"] = self.source_id - self.params["destination_id"] = self.data.peer_id self.params["rel_node_id"] = self.data.rel_node_id self.params["name"] = self.schema.identifier self.params["branch"] = self.branch.name @@ -443,9 +450,10 @@ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG # ----------------------------------------------------------------------- # Match all nodes, including properties # ----------------------------------------------------------------------- + + self.add_source_match_to_query(source_branch=self.source.get_branch_based_on_support_type()) + self.add_dest_match_to_query(destination_branch=self.branch, destination_id=self.data.peer_id) query = """ - MATCH (s:Node { uuid: $source_id }) - MATCH (d:Node { uuid: $destination_id }) MATCH (rl:Relationship { uuid: $rel_node_id }) """ self.add_to_query(query) @@ -497,8 +505,6 @@ def __init__(self, **kwargs): async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002 rel_filter, rel_params = self.branch.get_query_filter_path(at=self.at, variable_name="edge") - self.params["source_id"] = self.source_id - self.params["destination_id"] = self.destination_id self.params["rel_id"] = self.rel.id self.params["branch"] = self.branch.name self.params["rel_prop"] = self.get_relationship_properties_dict(status=RelationshipStatus.DELETED) @@ -509,9 +515,14 @@ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG r1 = f"{arrows.left.start}[r1:{self.rel_type} $rel_prop ]{arrows.left.end}" r2 = f"{arrows.right.start}[r2:{self.rel_type} $rel_prop ]{arrows.right.end}" + self.add_source_match_to_query(source_branch=self.source.get_branch_based_on_support_type()) + self.add_dest_match_to_query( + destination_branch=self.destination.get_branch_based_on_support_type(), + destination_id=self.destination_id or self.destination.get_id(), + ) query = """ - MATCH (s:Node { uuid: $source_id })-[:IS_RELATED]-(rl:Relationship {uuid: $rel_id})-[:IS_RELATED]-(d:Node { uuid: $destination_id }) - WITH s, rl, d + MATCH (s)-[:IS_RELATED]-(rl:Relationship {uuid: $rel_id})-[:IS_RELATED]-(d) + WITH DISTINCT s, rl, d LIMIT 1 CREATE (s)%(r1)s(rl) CREATE (rl)%(r2)s(d) @@ -853,8 +864,6 @@ class RelationshipGetQuery(RelationshipQuery): type: QueryType = QueryType.READ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002 - self.params["source_id"] = self.source_id - self.params["destination_id"] = self.destination_id self.params["name"] = self.schema.identifier self.params["branch"] = self.branch.name @@ -868,9 +877,12 @@ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG r1 = f"{arrows.left.start}[r1:{self.rel.rel_type}]{arrows.left.end}" r2 = f"{arrows.right.start}[r2:{self.rel.rel_type}]{arrows.right.end}" + self.add_source_match_to_query(source_branch=self.source.get_branch_based_on_support_type()) + self.add_dest_match_to_query( + destination_branch=self.destination.get_branch_based_on_support_type(), + destination_id=self.destination_id or self.destination.get_id(), + ) query = """ - MATCH (s:Node { uuid: $source_id }) - MATCH (d:Node { uuid: $destination_id }) MATCH (s)%s(rl:Relationship { name: $name })%s(d) WHERE %s """ % ( @@ -1097,7 +1109,11 @@ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG CALL { WITH rl MATCH (rl)-[active_edge:IS_RELATED]->(n) - WHERE %(active_rel_filter)s AND active_edge.status ="active" + WHERE %(active_rel_filter)s + WITH rl, active_edge, n + ORDER BY %(id_func)s(rl), %(id_func)s(n), active_edge.from DESC + WITH rl, n, head(collect(active_edge)) AS active_edge + WHERE active_edge.status = "active" CREATE (rl)-[deleted_edge:IS_RELATED $rel_prop]->(n) SET deleted_edge.hierarchy = active_edge.hierarchy WITH rl, active_edge, n @@ -1113,7 +1129,11 @@ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG WITH rl MATCH (rl)<-[active_edge:IS_RELATED]-(n) - WHERE %(active_rel_filter)s AND active_edge.status ="active" + WHERE %(active_rel_filter)s + WITH rl, active_edge, n + ORDER BY %(id_func)s(rl), %(id_func)s(n), active_edge.from DESC + WITH rl, n, head(collect(active_edge)) AS active_edge + WHERE active_edge.status = "active" CREATE (rl)<-[deleted_edge:IS_RELATED $rel_prop]-(n) SET deleted_edge.hierarchy = active_edge.hierarchy WITH rl, active_edge, n @@ -1126,9 +1146,7 @@ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG "inbound" as rel_direction } RETURN DISTINCT uuid, kind, rel_identifier, rel_direction - """ % { - "active_rel_filter": active_rel_filter, - } + """ % {"active_rel_filter": active_rel_filter, "id_func": db.get_id_function_name()} self.add_to_query(query) diff --git a/backend/infrahub/core/relationship/model.py b/backend/infrahub/core/relationship/model.py index 4f61a2905e..1df64b3287 100644 --- a/backend/infrahub/core/relationship/model.py +++ b/backend/infrahub/core/relationship/model.py @@ -416,7 +416,7 @@ async def delete(self, db: InfrahubDatabase, at: Timestamp | None = None) -> Non await update_relationships_to(rel_ids_to_update, to=delete_at, db=db) delete_query = await RelationshipDeleteQuery.init( - db=db, rel=self, source_id=node.id, destination_id=peer.id, branch=branch, at=delete_at + db=db, rel=self, source=node, destination=peer, branch=branch, at=delete_at ) await delete_query.execute(db=db) diff --git a/backend/tests/helpers/db_validation.py b/backend/tests/helpers/db_validation.py index c9a62398c5..8bad87b596 100644 --- a/backend/tests/helpers/db_validation.py +++ b/backend/tests/helpers/db_validation.py @@ -102,3 +102,31 @@ async def validate_node_relationships(node: Node, branch: Branch, db: InfrahubDa for result in query.results: print(result) assert len(result.data) == 1 and result.data[0] == "Edges state is correct" + + +async def verify_no_duplicate_paths(db: InfrahubDatabase) -> None: + """Verify that no duplicate paths exist at the database level""" + query = """ +MATCH path = (p)-[e]->(q) +WITH + %(id_func)s(p) AS node_id1, + e.branch AS branch, + e.from AS from_time, + type(e) AS edge_type, + %(id_func)s(q) AS node_id2, + path +WITH node_id1, branch, from_time, edge_type, node_id2, size(collect(path)) AS num_paths +WHERE num_paths > 1 +RETURN node_id1, branch, from_time, edge_type, node_id2, num_paths + """ % {"id_func": db.get_id_function_name()} + records = await db.execute_query(query=query) + for record in records: + node_id1 = record.get("node_id1") + branch = record.get("branch") + from_time = record.get("from_time") + edge_type = record.get("edge_type") + node_id2 = record.get("node_id2") + num_paths = record.get("num_paths") + raise ValueError( + f"{num_paths} paths ({branch=},{edge_type=},{from_time=}) between nodes '{node_id1}' and '{node_id2}'" + ) diff --git a/backend/tests/unit/core/diff/test_diff_and_merge.py b/backend/tests/unit/core/diff/test_diff_and_merge.py index 2273484c15..6e9598a150 100644 --- a/backend/tests/unit/core/diff/test_diff_and_merge.py +++ b/backend/tests/unit/core/diff/test_diff_and_merge.py @@ -21,35 +21,13 @@ from infrahub.core.timestamp import Timestamp from infrahub.database import InfrahubDatabase from infrahub.dependencies.registry import get_component_registry +from tests.helpers.db_validation import verify_no_duplicate_paths from tests.unit.conftest import _build_hierarchical_location_data from tests.unit.core.test_utils import verify_all_linked_edges_deleted from .get_one_node import get_one_diff_node -async def verify_no_duplicate_paths(db: InfrahubDatabase) -> None: - """Verify that no duplicate paths exist at the database level""" - query = """ -MATCH path = (p)-[e]->(q) -WITH COALESCE(p.uuid, p.value) AS node_id1, e.branch AS branch, e.from AS from_time, type(e) AS edge_type, COALESCE(q.uuid, q.value) AS node_id2, path -WHERE node_id1 IS NOT NULL AND node_id2 IS NOT NULL -WITH node_id1, branch, from_time, edge_type, node_id2, size(collect(path)) AS num_paths -WHERE num_paths > 1 -RETURN node_id1, branch, from_time, edge_type, node_id2, num_paths - """ - records = await db.execute_query(query=query) - for record in records: - node_id1 = record.get("node_id1") - branch = record.get("branch") - from_time = record.get("from_time") - edge_type = record.get("edge_type") - node_id2 = record.get("node_id2") - num_paths = record.get("num_paths") - raise ValueError( - f"{num_paths} paths ({branch=},{edge_type=},{from_time=}) between nodes '{node_id1}' and '{node_id2}'" - ) - - class TestDiffAndMerge: @pytest.fixture async def diff_repository(self, db: InfrahubDatabase, default_branch: Branch) -> DiffRepository: diff --git a/backend/tests/unit/core/test_relationship.py b/backend/tests/unit/core/test_relationship.py index 6b2c90eadd..0613a696ca 100644 --- a/backend/tests/unit/core/test_relationship.py +++ b/backend/tests/unit/core/test_relationship.py @@ -437,3 +437,49 @@ async def test_relationship_assign_from_pool( await obj.save(db=db) assert await obj.prefix.get_peer(db=db) + + +async def test_relationship_timestamp_changes( + db: InfrahubDatabase, person_jack_main: Node, tag_blue_main: Node, tag_red_main: Node, branch: Branch +): + # test going back in time after adding a relationship + before_add = Timestamp() + person_jack = await NodeManager.get_one(db=db, branch=branch, id=person_jack_main.id) + await person_jack.tags.update(db=db, data=[tag_blue_main.id]) + await person_jack.save(db=db) + before_add_person_jack = await NodeManager.get_one( + db=db, branch=branch, id=person_jack_main.id, at=before_add, prefetch_relationships=True + ) + tag_rels = await before_add_person_jack.tags.get_relationships(db=db) + assert not tag_rels + + # test going back in time after deleting a relationship + before_remove = Timestamp() + person_jack = await NodeManager.get_one(db=db, branch=branch, id=person_jack_main.id) + await person_jack.tags.update(db=db, data=[None]) + await person_jack.save(db=db) + before_remove_person_jack = await NodeManager.get_one( + db=db, branch=branch, id=person_jack_main.id, at=before_remove, prefetch_relationships=True + ) + tag_rels = await before_remove_person_jack.tags.get_relationships(db=db) + assert len(tag_rels) == 1 + assert [r.peer_id for r in tag_rels] == [tag_blue_main.id] + + # test with manually set save time + save_time = Timestamp() + before_save = save_time.add(microseconds=-1) + after_save = save_time.add(microseconds=1) + person_jack = await NodeManager.get_one(db=db, branch=branch, id=person_jack_main.id) + await person_jack.tags.update(db=db, data=[tag_red_main.id]) + await person_jack.save(db=db, at=save_time) + before_save_person_jack = await NodeManager.get_one( + db=db, branch=branch, id=person_jack_main.id, at=before_save, prefetch_relationships=True + ) + tag_rels = await before_save_person_jack.tags.get_relationships(db=db) + assert len(tag_rels) == 0 + after_save_person_jack = await NodeManager.get_one( + db=db, branch=branch, id=person_jack_main.id, at=after_save, prefetch_relationships=True + ) + tag_rels = await after_save_person_jack.tags.get_relationships(db=db) + assert len(tag_rels) == 1 + assert [r.peer_id for r in tag_rels] == [tag_red_main.id] diff --git a/backend/tests/unit/core/test_relationship_query.py b/backend/tests/unit/core/test_relationship_query.py index 2b849d208e..3e25b54319 100644 --- a/backend/tests/unit/core/test_relationship_query.py +++ b/backend/tests/unit/core/test_relationship_query.py @@ -26,6 +26,7 @@ from infrahub.core.timestamp import Timestamp from infrahub.core.utils import get_paths_between_nodes from infrahub.database import InfrahubDatabase +from tests.helpers.db_validation import verify_no_duplicate_paths class DummyRelationshipQuery(RelationshipQuery): @@ -244,6 +245,7 @@ async def test_query_RelationshipCreateQuery_for_node_with_migrated_kind( relationships=["IS_RELATED"], ) assert len(paths) == 0 + await verify_no_duplicate_paths(db=db) async def test_query_RelationshipDeleteQuery( @@ -377,6 +379,55 @@ def get_active_path_and_rel(all_paths, previous_rel: str): assert len(paths) == 8 +async def test_query_RelationshipDeleteQuery_on_migrated_kind_node( + db: InfrahubDatabase, tag_blue_main: Node, person_jack_tags_main: Node, branch: Branch +): + person_schema = registry.schema.get(name="TestPerson") + rel_schema = person_schema.get_relationship("tags") + paths = await get_paths_between_nodes( + db=db, + source_id=tag_blue_main.db_id, + destination_id=person_jack_tags_main.db_id, + max_length=2, + relationships=["IS_RELATED"], + ) + assert len(paths) == 1 + + # migrate person kind + person_schema.name = "NewPerson" + person_schema.namespace = "Test2" + assert person_schema.kind == "Test2NewPerson" + registry.schema.set(name="Test2NewPerson", schema=person_schema, branch=branch.name) + migration = NodeKindUpdateMigration( + previous_node_schema=registry.schema.get(name="TestPerson", branch=branch), + new_node_schema=person_schema, + schema_path=SchemaPath( + path_type=SchemaPathType.ATTRIBUTE, schema_kind="Test2NewPerson", field_name="namespace" + ), + ) + execution_result = await migration.execute(db=db, branch=branch) + assert not execution_result.errors + + migrated_jack = await NodeManager.get_one(db=db, branch=branch, id=person_jack_tags_main.id) + tag_rels = await migrated_jack.tags.get_relationships(db=db) + assert len(tag_rels) == 2 + blue_tag_rels = [tag_rel for tag_rel in tag_rels if tag_rel.peer_id == tag_blue_main.id] + assert len(blue_tag_rels) == 1 + blue_tag_rel = blue_tag_rels[0] + + query = await RelationshipDeleteQuery.init( + db=db, + source=migrated_jack, + destination=tag_blue_main, + schema=rel_schema, + rel=blue_tag_rel, + branch=branch, + at=Timestamp(), + ) + await query.execute(db=db) + await verify_no_duplicate_paths(db=db) + + async def test_relationship_delete_peer(db: InfrahubDatabase, default_branch, tag_blue_main: Node): person = await Node.init(db=db, branch=default_branch, schema="TestPerson") await person.new(db=db, firstname="Kara", lastname="Thrace", tags=[tag_blue_main]) @@ -805,6 +856,92 @@ async def test_query_RelationshipDataDeleteQuery( assert len(paths) == 4 +async def test_query_RelationshipDataDeleteQuery_on_migrated_kind_node( + db: InfrahubDatabase, tag_blue_main: Node, tag_red_main: Node, person_jack_tags_main: Node, branch: Branch +): + person_schema = registry.schema.get(name="TestPerson", branch=branch) + rel_schema = person_schema.get_relationship("tags") + + # migrate person kind + person_schema.name = "NewPerson" + person_schema.namespace = "Test2" + assert person_schema.kind == "Test2NewPerson" + registry.schema.set(name="Test2NewPerson", schema=person_schema, branch=branch.name) + migration = NodeKindUpdateMigration( + previous_node_schema=registry.schema.get(name="TestPerson", branch=branch), + new_node_schema=person_schema, + schema_path=SchemaPath( + path_type=SchemaPathType.ATTRIBUTE, schema_kind="Test2NewPerson", field_name="namespace" + ), + ) + execution_result = await migration.execute(db=db, branch=branch) + assert not execution_result.errors + + migrated_jack = await NodeManager.get_one(db=db, branch=branch, id=person_jack_tags_main.id) + # Query the existing relationship in RelationshipPeerData format + query1 = await RelationshipGetPeerQuery.init( + db=db, + source=migrated_jack, + schema=rel_schema, + rel=Relationship(schema=rel_schema, branch=branch, node=migrated_jack), + ) + await query1.execute(db=db) + peers_database: dict[str, RelationshipPeerData] = {peer.peer_id: peer for peer in query1.get_peers()} + + # Delete the relationship + query2 = await RelationshipDataDeleteQuery.init( + db=db, + branch=branch, + source=migrated_jack, + data=peers_database[tag_blue_main.id], + schema=rel_schema, + rel=Relationship, + ) + await query2.execute(db=db) + await verify_no_duplicate_paths(db=db) + + # migrate tag kind + tag_schema = registry.schema.get("BuiltinTag", branch=branch) + tag_schema.name = "NewTag" + tag_schema.namespace = "Builtin2" + assert tag_schema.kind == "Builtin2NewTag" + registry.schema.set(name="Builtin2NewTag", schema=tag_schema, branch=branch.name) + migration = NodeKindUpdateMigration( + previous_node_schema=registry.schema.get(name="BuiltinTag", branch=branch), + new_node_schema=tag_schema, + schema_path=SchemaPath( + path_type=SchemaPathType.ATTRIBUTE, schema_kind="Builtin2NewTag", field_name="namespace" + ), + ) + execution_result = await migration.execute(db=db, branch=branch) + assert not execution_result.errors + + # delete other tag relationship + rel_schema.peer = "Builtin2NewTag" + migrated_jack = await NodeManager.get_one(db=db, branch=branch, id=person_jack_tags_main.id) + # Query the existing relationship in RelationshipPeerData format + query1 = await RelationshipGetPeerQuery.init( + db=db, + source=migrated_jack, + schema=rel_schema, + rel=Relationship(schema=rel_schema, branch=branch, node=migrated_jack), + ) + await query1.execute(db=db) + peers_database: dict[str, RelationshipPeerData] = {peer.peer_id: peer for peer in query1.get_peers()} + + # Delete the relationship + query2 = await RelationshipDataDeleteQuery.init( + db=db, + branch=branch, + source=migrated_jack, + data=peers_database[tag_red_main.id], + schema=rel_schema, + rel=Relationship, + ) + await query2.execute(db=db) + await verify_no_duplicate_paths(db=db) + + async def test_query_RelationshipCountPerNodeQuery( db: InfrahubDatabase, person_john_main, diff --git a/changelog/+rel_create_on_migrated_kind_node.fixed.md b/changelog/+rel_create_on_migrated_kind_node.fixed.md index eb2118fba9..f7c0c808a0 100644 --- a/changelog/+rel_create_on_migrated_kind_node.fixed.md +++ b/changelog/+rel_create_on_migrated_kind_node.fixed.md @@ -1 +1 @@ -Prevent creating duplicate edges on the database when adding a relationship to a node that had its kind or inheritance updated \ No newline at end of file +Prevent creating duplicate edges on the database when adding a relationship to or deleting a relationship from a node that had its kind or inheritance updated \ No newline at end of file diff --git a/python_sdk b/python_sdk index 795980a5a7..5d99664f85 160000 --- a/python_sdk +++ b/python_sdk @@ -1 +1 @@ -Subproject commit 795980a5a7c21b198e7f67e603818311cd23a825 +Subproject commit 5d99664f850005dca218a3d3f97eec57f90d8f2e