From 2f591c0ac51f98317c123bb82add234810a4450d Mon Sep 17 00:00:00 2001 From: Aaron McCarty Date: Thu, 8 May 2025 17:22:37 -0700 Subject: [PATCH 01/11] update rel cypher queries for migrated kind nodes --- backend/infrahub/core/query/relationship.py | 110 +++++++++++--------- backend/infrahub/core/relationship/model.py | 2 +- 2 files changed, 60 insertions(+), 52 deletions(-) diff --git a/backend/infrahub/core/query/relationship.py b/backend/infrahub/core/query/relationship.py index 2c8fd3ee3e..ccc843b1ad 100644 --- a/backend/infrahub/core/query/relationship.py +++ b/backend/infrahub/core/query/relationship.py @@ -205,50 +205,18 @@ 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)) """ 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 @@ -260,10 +228,11 @@ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG } 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 }) @@ -289,6 +258,41 @@ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG 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 +437,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 +446,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 +501,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 +511,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 +860,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 +873,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 """ % ( 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) From c7866f8010a51565cdd9d9b958a6d252cb4c37dd Mon Sep 17 00:00:00 2001 From: Aaron McCarty Date: Thu, 8 May 2025 18:26:52 -0700 Subject: [PATCH 02/11] more tests --- backend/infrahub/core/query/node.py | 24 ++- backend/tests/helpers/db_validation.py | 28 ++++ .../unit/core/diff/test_diff_and_merge.py | 24 +-- .../unit/core/test_relationship_query.py | 137 ++++++++++++++++++ 4 files changed, 188 insertions(+), 25 deletions(-) diff --git a/backend/infrahub/core/query/node.py b/backend/infrahub/core/query/node.py index cbd45438f4..14f37666d6 100644 --- a/backend/infrahub/core/query/node.py +++ b/backend/infrahub/core/query/node.py @@ -486,8 +486,28 @@ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG ) self.params.update(branch_params) - query = """ - MATCH (n:Node) WHERE n.uuid IN $ids + if not self.branch_agnostic and (self.branch.is_default or self.branch.is_global): + query = """ + MATCH (n:Node) + WHERE n.uuid IN $ids + AND NOT exists((n)-[:IS_PART_OF {branch: $branch_name, status: "deleted"}]->(:Root)) + """ + self.params["branch_name"] = self.branch.name + else: + query = """ + MATCH (n:Node) + WHERE n.uuid IN $ids + CALL { + WITH n + MATCH (n)-[r:IS_PART_OF]->(:Root) + WHERE %(branch_filter)s + RETURN r.status = "active" AS is_active + ORDER BY r.from DESC + LIMIT 1 + } + WITH n WHERE is_active = TRUE + """ % {"branch_filter": branch_filter} + query += """ MATCH (n)-[:HAS_ATTRIBUTE]-(a:Attribute) """ if self.fields: 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_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, From 9330c988b0c3553850117c60ba9e48eff6f8ddb7 Mon Sep 17 00:00:00 2001 From: Aaron McCarty Date: Thu, 8 May 2025 18:27:44 -0700 Subject: [PATCH 03/11] update changelog --- changelog/+rel_create_on_migrated_kind_node.fixed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 91cffad8429b7d72cfb195e877a2823a11f4ed01 Mon Sep 17 00:00:00 2001 From: Aaron McCarty Date: Fri, 9 May 2025 07:05:28 -0700 Subject: [PATCH 04/11] remove accidental change --- backend/infrahub/core/query/node.py | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/backend/infrahub/core/query/node.py b/backend/infrahub/core/query/node.py index 14f37666d6..cbd45438f4 100644 --- a/backend/infrahub/core/query/node.py +++ b/backend/infrahub/core/query/node.py @@ -486,28 +486,8 @@ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG ) self.params.update(branch_params) - if not self.branch_agnostic and (self.branch.is_default or self.branch.is_global): - query = """ - MATCH (n:Node) - WHERE n.uuid IN $ids - AND NOT exists((n)-[:IS_PART_OF {branch: $branch_name, status: "deleted"}]->(:Root)) - """ - self.params["branch_name"] = self.branch.name - else: - query = """ - MATCH (n:Node) - WHERE n.uuid IN $ids - CALL { - WITH n - MATCH (n)-[r:IS_PART_OF]->(:Root) - WHERE %(branch_filter)s - RETURN r.status = "active" AS is_active - ORDER BY r.from DESC - LIMIT 1 - } - WITH n WHERE is_active = TRUE - """ % {"branch_filter": branch_filter} - query += """ + query = """ + MATCH (n:Node) WHERE n.uuid IN $ids MATCH (n)-[:HAS_ATTRIBUTE]-(a:Attribute) """ if self.fields: From 5694b2d9b9618fe5c47d30540f30639afc59f96d Mon Sep 17 00:00:00 2001 From: Aaron McCarty Date: Fri, 9 May 2025 07:06:16 -0700 Subject: [PATCH 05/11] account for timestamp in simple relationship query --- backend/infrahub/core/query/relationship.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/backend/infrahub/core/query/relationship.py b/backend/infrahub/core/query/relationship.py index ccc843b1ad..ff2963a1c0 100644 --- a/backend/infrahub/core/query/relationship.py +++ b/backend/infrahub/core/query/relationship.py @@ -210,7 +210,9 @@ def add_source_match_to_query(self, source_branch: Branch) -> None: 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 source_filter, source_filter_params = source_branch.get_query_filter_path( @@ -226,7 +228,7 @@ def add_source_match_to_query(self, source_branch: Branch) -> None: 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.add_to_query(source_query_match) @@ -236,7 +238,9 @@ def add_dest_match_to_query(self, destination_branch: Branch, destination_id: st 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: @@ -253,7 +257,7 @@ def add_dest_match_to_query(self, destination_branch: Branch, destination_id: st 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) From 55c44f792c11ffe5de7ce79df87c824f4e180375 Mon Sep 17 00:00:00 2001 From: Aaron McCarty Date: Fri, 9 May 2025 08:33:29 -0700 Subject: [PATCH 06/11] test for timestamp stuff on relationships --- backend/tests/unit/core/test_relationship.py | 46 ++++++++++++++++++++ python_sdk | 2 +- 2 files changed, 47 insertions(+), 1 deletion(-) 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/python_sdk b/python_sdk index 795980a5a7..6b6028641e 160000 --- a/python_sdk +++ b/python_sdk @@ -1 +1 @@ -Subproject commit 795980a5a7c21b198e7f67e603818311cd23a825 +Subproject commit 6b6028641eb007ee446a06ca3d981f58c007387c From d7939c7774172498c55e43475755f9b0b82f9e66 Mon Sep 17 00:00:00 2001 From: Aaron McCarty Date: Fri, 9 May 2025 18:11:37 -0700 Subject: [PATCH 07/11] same updates for node and relationship delete queries --- backend/infrahub/core/query/node.py | 25 ++- backend/infrahub/core/query/relationship.py | 172 +++++++++++++++++++- 2 files changed, 194 insertions(+), 3 deletions(-) diff --git a/backend/infrahub/core/query/node.py b/backend/infrahub/core/query/node.py index cbd45438f4..bb11deda5c 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 ff2963a1c0..336818d046 100644 --- a/backend/infrahub/core/query/relationship.py +++ b/backend/infrahub/core/query/relationship.py @@ -1109,7 +1109,12 @@ 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 active_edge.from DESC + LIMIT 1 + WITH rl, active_edge, n + 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 @@ -1125,7 +1130,12 @@ 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 active_edge.from DESC + LIMIT 1 + WITH rl, active_edge, n + 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 @@ -1143,6 +1153,164 @@ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG } self.add_to_query(query) + # { + # 'source_id': '183e023e-bc59-559c-43e3-1677a0b2bdfc', + # 'branch': 'branch2', + # 'rel_prop': {'branch': 'branch2', 'branch_level': 2, 'status': 'deleted', 'from': '2025-05-10T00:17:45.601332Z'}, + # 'at': '2025-05-10T00:17:45.601332Z', + # 'branch0': ['-global-', 'main'], + # 'time0': '2025-05-10T00:16:39.992858Z', + # 'branch1': ['-global-', 'branch2'], + # 'time1': '2025-05-10T00:17:45.601332Z' + # } + """ + MATCH (s:Node { uuid: $source_id })-[active_edge:IS_RELATED]-(rl:Relationship) + WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND active_edge.status = "active" + WITH DISTINCT rl + CALL { + WITH rl + MATCH (rl)<-[active_edge:IS_VISIBLE]-(n) + WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND active_edge.status ="active" + CREATE (rl)<-[deleted_edge:IS_VISIBLE $rel_prop]-(n) + SET deleted_edge.hierarchy = active_edge.hierarchy + WITH active_edge, n + WHERE active_edge.branch = $branch AND active_edge.to IS NULL + SET active_edge.to = $at + } + CALL { + WITH rl + MATCH (rl)<-[active_edge:IS_PROTECTED]-(n) + WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND active_edge.status ="active" + CREATE (rl)<-[deleted_edge:IS_PROTECTED $rel_prop]-(n) + SET deleted_edge.hierarchy = active_edge.hierarchy + WITH active_edge, n + WHERE active_edge.branch = $branch AND active_edge.to IS NULL + SET active_edge.to = $at + } + CALL { + WITH rl + MATCH (rl)<-[active_edge:HAS_OWNER]-(n) + WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND active_edge.status ="active" + CREATE (rl)<-[deleted_edge:HAS_OWNER $rel_prop]-(n) + SET deleted_edge.hierarchy = active_edge.hierarchy + WITH active_edge, n + WHERE active_edge.branch = $branch AND active_edge.to IS NULL + SET active_edge.to = $at + } + CALL { + WITH rl + MATCH (rl)<-[active_edge:HAS_SOURCE]-(n) + WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND active_edge.status ="active" + CREATE (rl)<-[deleted_edge:HAS_SOURCE $rel_prop]-(n) + SET deleted_edge.hierarchy = active_edge.hierarchy + WITH active_edge, n + WHERE active_edge.branch = $branch AND active_edge.to IS NULL + SET active_edge.to = $at + } + CALL { + WITH rl + MATCH (rl)-[active_edge:IS_VISIBLE]->(n) + WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND active_edge.status ="active" + CREATE (rl)-[deleted_edge:IS_VISIBLE $rel_prop]->(n) + SET deleted_edge.hierarchy = active_edge.hierarchy + WITH active_edge, n + WHERE active_edge.branch = $branch AND active_edge.to IS NULL + SET active_edge.to = $at + } + CALL { + WITH rl + MATCH (rl)-[active_edge:IS_PROTECTED]->(n) + WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND active_edge.status ="active" + CREATE (rl)-[deleted_edge:IS_PROTECTED $rel_prop]->(n) + SET deleted_edge.hierarchy = active_edge.hierarchy + WITH active_edge, n + WHERE active_edge.branch = $branch AND active_edge.to IS NULL + SET active_edge.to = $at + } + CALL { + WITH rl + MATCH (rl)-[active_edge:HAS_OWNER]->(n) + WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND active_edge.status ="active" + CREATE (rl)-[deleted_edge:HAS_OWNER $rel_prop]->(n) + SET deleted_edge.hierarchy = active_edge.hierarchy + WITH active_edge, n + WHERE active_edge.branch = $branch AND active_edge.to IS NULL + SET active_edge.to = $at + } + CALL { + WITH rl + MATCH (rl)-[active_edge:HAS_SOURCE]->(n) + WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND active_edge.status ="active" + CREATE (rl)-[deleted_edge:HAS_SOURCE $rel_prop]->(n) + SET deleted_edge.hierarchy = active_edge.hierarchy + WITH active_edge, n + WHERE active_edge.branch = $branch AND active_edge.to IS NULL + SET active_edge.to = $at + } + CALL { + WITH rl + MATCH (rl)-[active_edge:IS_RELATED]->(n) + WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND 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 + WHERE active_edge.branch = $branch AND active_edge.to IS NULL + SET active_edge.to = $at + RETURN + n.uuid as uuid, + n.kind as kind, + rl.name as rel_identifier, + "outbound" as rel_direction + UNION + WITH rl + MATCH (rl)<-[active_edge:IS_RELATED]-(n) + WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) + OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND 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 + WHERE active_edge.branch = $branch AND active_edge.to IS NULL + SET active_edge.to = $at + RETURN + n.uuid as uuid, + n.kind as kind, + rl.name as rel_identifier, + "inbound" as rel_direction + } + RETURN DISTINCT uuid, kind, rel_identifier, rel_direction + """ def get_deleted_relationships_changelog( self, node_schema: NodeSchema From 8dd6fc8a8348b07fb6dd55877c6ca9b824a180dd Mon Sep 17 00:00:00 2001 From: Aaron McCarty Date: Mon, 12 May 2025 10:21:08 -0700 Subject: [PATCH 08/11] update sdk commit --- python_sdk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_sdk b/python_sdk index 6b6028641e..5d99664f85 160000 --- a/python_sdk +++ b/python_sdk @@ -1 +1 @@ -Subproject commit 6b6028641eb007ee446a06ca3d981f58c007387c +Subproject commit 5d99664f850005dca218a3d3f97eec57f90d8f2e From 54840ca986539db51e4b168447c22d82ff056fe6 Mon Sep 17 00:00:00 2001 From: Aaron McCarty Date: Mon, 12 May 2025 10:45:57 -0700 Subject: [PATCH 09/11] remove comment from testing --- backend/infrahub/core/query/relationship.py | 158 -------------------- 1 file changed, 158 deletions(-) diff --git a/backend/infrahub/core/query/relationship.py b/backend/infrahub/core/query/relationship.py index 336818d046..1f3b7c21d1 100644 --- a/backend/infrahub/core/query/relationship.py +++ b/backend/infrahub/core/query/relationship.py @@ -1153,164 +1153,6 @@ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG } self.add_to_query(query) - # { - # 'source_id': '183e023e-bc59-559c-43e3-1677a0b2bdfc', - # 'branch': 'branch2', - # 'rel_prop': {'branch': 'branch2', 'branch_level': 2, 'status': 'deleted', 'from': '2025-05-10T00:17:45.601332Z'}, - # 'at': '2025-05-10T00:17:45.601332Z', - # 'branch0': ['-global-', 'main'], - # 'time0': '2025-05-10T00:16:39.992858Z', - # 'branch1': ['-global-', 'branch2'], - # 'time1': '2025-05-10T00:17:45.601332Z' - # } - """ - MATCH (s:Node { uuid: $source_id })-[active_edge:IS_RELATED]-(rl:Relationship) - WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND active_edge.status = "active" - WITH DISTINCT rl - CALL { - WITH rl - MATCH (rl)<-[active_edge:IS_VISIBLE]-(n) - WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND active_edge.status ="active" - CREATE (rl)<-[deleted_edge:IS_VISIBLE $rel_prop]-(n) - SET deleted_edge.hierarchy = active_edge.hierarchy - WITH active_edge, n - WHERE active_edge.branch = $branch AND active_edge.to IS NULL - SET active_edge.to = $at - } - CALL { - WITH rl - MATCH (rl)<-[active_edge:IS_PROTECTED]-(n) - WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND active_edge.status ="active" - CREATE (rl)<-[deleted_edge:IS_PROTECTED $rel_prop]-(n) - SET deleted_edge.hierarchy = active_edge.hierarchy - WITH active_edge, n - WHERE active_edge.branch = $branch AND active_edge.to IS NULL - SET active_edge.to = $at - } - CALL { - WITH rl - MATCH (rl)<-[active_edge:HAS_OWNER]-(n) - WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND active_edge.status ="active" - CREATE (rl)<-[deleted_edge:HAS_OWNER $rel_prop]-(n) - SET deleted_edge.hierarchy = active_edge.hierarchy - WITH active_edge, n - WHERE active_edge.branch = $branch AND active_edge.to IS NULL - SET active_edge.to = $at - } - CALL { - WITH rl - MATCH (rl)<-[active_edge:HAS_SOURCE]-(n) - WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND active_edge.status ="active" - CREATE (rl)<-[deleted_edge:HAS_SOURCE $rel_prop]-(n) - SET deleted_edge.hierarchy = active_edge.hierarchy - WITH active_edge, n - WHERE active_edge.branch = $branch AND active_edge.to IS NULL - SET active_edge.to = $at - } - CALL { - WITH rl - MATCH (rl)-[active_edge:IS_VISIBLE]->(n) - WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND active_edge.status ="active" - CREATE (rl)-[deleted_edge:IS_VISIBLE $rel_prop]->(n) - SET deleted_edge.hierarchy = active_edge.hierarchy - WITH active_edge, n - WHERE active_edge.branch = $branch AND active_edge.to IS NULL - SET active_edge.to = $at - } - CALL { - WITH rl - MATCH (rl)-[active_edge:IS_PROTECTED]->(n) - WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND active_edge.status ="active" - CREATE (rl)-[deleted_edge:IS_PROTECTED $rel_prop]->(n) - SET deleted_edge.hierarchy = active_edge.hierarchy - WITH active_edge, n - WHERE active_edge.branch = $branch AND active_edge.to IS NULL - SET active_edge.to = $at - } - CALL { - WITH rl - MATCH (rl)-[active_edge:HAS_OWNER]->(n) - WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND active_edge.status ="active" - CREATE (rl)-[deleted_edge:HAS_OWNER $rel_prop]->(n) - SET deleted_edge.hierarchy = active_edge.hierarchy - WITH active_edge, n - WHERE active_edge.branch = $branch AND active_edge.to IS NULL - SET active_edge.to = $at - } - CALL { - WITH rl - MATCH (rl)-[active_edge:HAS_SOURCE]->(n) - WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND active_edge.status ="active" - CREATE (rl)-[deleted_edge:HAS_SOURCE $rel_prop]->(n) - SET deleted_edge.hierarchy = active_edge.hierarchy - WITH active_edge, n - WHERE active_edge.branch = $branch AND active_edge.to IS NULL - SET active_edge.to = $at - } - CALL { - WITH rl - MATCH (rl)-[active_edge:IS_RELATED]->(n) - WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND 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 - WHERE active_edge.branch = $branch AND active_edge.to IS NULL - SET active_edge.to = $at - RETURN - n.uuid as uuid, - n.kind as kind, - rl.name as rel_identifier, - "outbound" as rel_direction - UNION - WITH rl - MATCH (rl)<-[active_edge:IS_RELATED]-(n) - WHERE ((active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch0 AND active_edge.from <= $time0 AND active_edge.to >= $time0) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to IS NULL) - OR (active_edge.branch IN $branch1 AND active_edge.from <= $time1 AND active_edge.to >= $time1)) AND 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 - WHERE active_edge.branch = $branch AND active_edge.to IS NULL - SET active_edge.to = $at - RETURN - n.uuid as uuid, - n.kind as kind, - rl.name as rel_identifier, - "inbound" as rel_direction - } - RETURN DISTINCT uuid, kind, rel_identifier, rel_direction - """ def get_deleted_relationships_changelog( self, node_schema: NodeSchema From c6616d40a7bbe790b718aec8cb5bd1dbdf30331c Mon Sep 17 00:00:00 2001 From: Aaron McCarty Date: Mon, 12 May 2025 10:46:25 -0700 Subject: [PATCH 10/11] fix indent error --- backend/infrahub/core/query/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/infrahub/core/query/node.py b/backend/infrahub/core/query/node.py index bb11deda5c..ac96266c2e 100644 --- a/backend/infrahub/core/query/node.py +++ b/backend/infrahub/core/query/node.py @@ -434,7 +434,7 @@ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG } WITH n WHERE is_active = TRUE """ % {"node_filter": node_filter} - self.params.update(node_filter_params) + self.params.update(node_filter_params) self.add_to_query(node_query_match) query = """ From 849c1ce661263c01925d7eae4d9e0148d41482d7 Mon Sep 17 00:00:00 2001 From: Aaron McCarty Date: Mon, 12 May 2025 12:54:32 -0700 Subject: [PATCH 11/11] fix DelationshipDeleteAllQuery update --- backend/infrahub/core/query/relationship.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/backend/infrahub/core/query/relationship.py b/backend/infrahub/core/query/relationship.py index 1f3b7c21d1..56ea01175f 100644 --- a/backend/infrahub/core/query/relationship.py +++ b/backend/infrahub/core/query/relationship.py @@ -1111,9 +1111,8 @@ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG MATCH (rl)-[active_edge:IS_RELATED]->(n) WHERE %(active_rel_filter)s WITH rl, active_edge, n - ORDER BY active_edge.from DESC - LIMIT 1 - 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 @@ -1132,9 +1131,8 @@ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG MATCH (rl)<-[active_edge:IS_RELATED]-(n) WHERE %(active_rel_filter)s WITH rl, active_edge, n - ORDER BY active_edge.from DESC - LIMIT 1 - 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 @@ -1148,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)