diff --git a/backend/infrahub/core/manager.py b/backend/infrahub/core/manager.py index 31fb943e4f..cfc9d5db98 100644 --- a/backend/infrahub/core/manager.py +++ b/backend/infrahub/core/manager.py @@ -1229,20 +1229,31 @@ async def _enrich_node_dicts_with_relationships( if not prefetch_relationships and not fields: return cardinality_one_identifiers_by_kind: dict[str, dict[str, RelationshipDirection]] | None = None - all_identifiers: list[str] | None = None + outbound_identifiers: set[str] | None = None + inbound_identifiers: set[str] | None = None + bidirectional_identifiers: set[str] | None = None if not prefetch_relationships: cardinality_one_identifiers_by_kind = _get_cardinality_one_identifiers_by_kind( nodes=nodes_by_id.values(), fields=fields or {} ) - all_identifiers_set: set[str] = set() + outbound_identifiers = set() + inbound_identifiers = set() + bidirectional_identifiers = set() for identifier_direction_map in cardinality_one_identifiers_by_kind.values(): - all_identifiers_set.update(identifier_direction_map.keys()) - all_identifiers = list(all_identifiers_set) + for identifier, direction in identifier_direction_map.items(): + if direction is RelationshipDirection.OUTBOUND: + outbound_identifiers.add(identifier) + elif direction is RelationshipDirection.INBOUND: + inbound_identifiers.add(identifier) + elif direction is RelationshipDirection.BIDIR: + bidirectional_identifiers.add(identifier) query = await NodeListGetRelationshipsQuery.init( db=db, ids=list(nodes_by_id.keys()), - relationship_identifiers=all_identifiers, + outbound_identifiers=None if outbound_identifiers is None else list(outbound_identifiers), + inbound_identifiers=None if inbound_identifiers is None else list(inbound_identifiers), + bidirectional_identifiers=None if bidirectional_identifiers is None else list(bidirectional_identifiers), branch=branch, at=at, branch_agnostic=branch_agnostic, diff --git a/backend/infrahub/core/query/node.py b/backend/infrahub/core/query/node.py index fabcc06a3a..2fe0d48757 100644 --- a/backend/infrahub/core/query/node.py +++ b/backend/infrahub/core/query/node.py @@ -649,51 +649,118 @@ class NodeListGetRelationshipsQuery(Query): type: QueryType = QueryType.READ insert_return: bool = False - def __init__(self, ids: list[str], relationship_identifiers: list[str] | None = None, **kwargs): + def __init__( + self, + ids: list[str], + outbound_identifiers: list[str] | None = None, + inbound_identifiers: list[str] | None = None, + bidirectional_identifiers: list[str] | None = None, + **kwargs, + ): self.ids = ids - self.relationship_identifiers = relationship_identifiers + self.outbound_identifiers = outbound_identifiers + self.inbound_identifiers = inbound_identifiers + self.bidirectional_identifiers = bidirectional_identifiers super().__init__(**kwargs) async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002 self.params["ids"] = self.ids - self.params["relationship_identifiers"] = self.relationship_identifiers + self.params["outbound_identifiers"] = self.outbound_identifiers + self.params["inbound_identifiers"] = self.inbound_identifiers + self.params["bidirectional_identifiers"] = self.bidirectional_identifiers rels_filter, rels_params = self.branch.get_query_filter_path(at=self.at, branch_agnostic=self.branch_agnostic) self.params.update(rels_params) query = """ MATCH (n:Node) WHERE n.uuid IN $ids - MATCH paths_in = ((n)<-[r1:IS_RELATED]-(rel:Relationship)<-[r2:IS_RELATED]-(peer)) - WHERE ($relationship_identifiers IS NULL OR rel.name in $relationship_identifiers) - AND all(r IN relationships(paths_in) WHERE (%(filters)s)) - AND n.uuid <> peer.uuid - RETURN n, rel, peer, r1, r2, "inbound" as direction - UNION - MATCH (n:Node) WHERE n.uuid IN $ids - MATCH paths_out = ((n)-[r1:IS_RELATED]->(rel:Relationship)-[r2:IS_RELATED]->(peer)) - WHERE ($relationship_identifiers IS NULL OR rel.name in $relationship_identifiers) - AND all(r IN relationships(paths_out) WHERE (%(filters)s)) - AND n.uuid <> peer.uuid - RETURN n, rel, peer, r1, r2, "outbound" as direction - UNION - MATCH (n:Node) WHERE n.uuid IN $ids - MATCH paths_bidir = ((n)-[r1:IS_RELATED]->(rel:Relationship)<-[r2:IS_RELATED]-(peer)) - WHERE ($relationship_identifiers IS NULL OR rel.name in $relationship_identifiers) - AND all(r IN relationships(paths_bidir) WHERE (%(filters)s)) - AND n.uuid <> peer.uuid - RETURN n, rel, peer, r1, r2, "bidirectional" as direction + CALL { + WITH n + MATCH (n)<-[:IS_RELATED]-(rel:Relationship)<-[:IS_RELATED]-(peer) + WHERE ($inbound_identifiers IS NULL OR rel.name in $inbound_identifiers) + AND n.uuid <> peer.uuid + WITH DISTINCT n, rel, peer + CALL { + WITH n, rel, peer + MATCH (n)<-[r:IS_RELATED]-(rel) + WHERE (%(filters)s) + WITH n, rel, peer, r + ORDER BY r.from DESC + LIMIT 1 + WITH n, rel, peer, r AS r1 + WHERE r1.status = "active" + MATCH (rel)<-[r:IS_RELATED]-(peer) + WHERE (%(filters)s) + WITH r1, r + ORDER BY r.from DESC + LIMIT 1 + WITH r1, r AS r2 + WHERE r2.status = "active" + RETURN 1 AS is_active + } + RETURN n.uuid AS n_uuid, rel.name AS rel_name, peer.uuid AS peer_uuid, "inbound" as direction + UNION + WITH n + MATCH (n)-[:IS_RELATED]->(rel:Relationship)-[:IS_RELATED]->(peer) + WHERE ($outbound_identifiers IS NULL OR rel.name in $outbound_identifiers) + AND n.uuid <> peer.uuid + WITH DISTINCT n, rel, peer + CALL { + WITH n, rel, peer + MATCH (n)-[r:IS_RELATED]->(rel) + WHERE (%(filters)s) + WITH n, rel, peer, r + ORDER BY r.from DESC + LIMIT 1 + WITH n, rel, peer, r AS r1 + WHERE r1.status = "active" + MATCH (rel)-[r:IS_RELATED]->(peer) + WHERE (%(filters)s) + WITH r1, r + ORDER BY r.from DESC + LIMIT 1 + WITH r1, r AS r2 + WHERE r2.status = "active" + RETURN 1 AS is_active + } + RETURN n.uuid AS n_uuid, rel.name AS rel_name, peer.uuid AS peer_uuid, "outbound" as direction + UNION + WITH n + MATCH (n)-[:IS_RELATED]->(rel:Relationship)<-[:IS_RELATED]-(peer) + WHERE ($bidirectional_identifiers IS NULL OR rel.name in $bidirectional_identifiers) + AND n.uuid <> peer.uuid + WITH DISTINCT n, rel, peer + CALL { + WITH n, rel, peer + MATCH (n)-[r:IS_RELATED]->(rel) + WHERE (%(filters)s) + WITH n, rel, peer, r + ORDER BY r.from DESC + LIMIT 1 + WITH n, rel, peer, r AS r1 + WHERE r1.status = "active" + MATCH (rel)<-[r:IS_RELATED]-(peer) + WHERE (%(filters)s) + WITH r1, r + ORDER BY r.from DESC + LIMIT 1 + WITH r1, r AS r2 + WHERE r2.status = "active" + RETURN 1 AS is_active + } + RETURN n.uuid AS n_uuid, rel.name AS rel_name, peer.uuid AS peer_uuid, "bidirectional" as direction + } + RETURN DISTINCT n_uuid, rel_name, peer_uuid, direction """ % {"filters": rels_filter} - self.add_to_query(query) - - self.return_labels = ["n", "rel", "peer", "r1", "r2", "direction"] + self.return_labels = ["n_uuid", "rel_name", "peer_uuid", "direction"] def get_peers_group_by_node(self) -> GroupedPeerNodes: gpn = GroupedPeerNodes() - for result in self.get_results_group_by(("n", "uuid"), ("rel", "name"), ("peer", "uuid")): - node_id = result.get("n").get("uuid") - rel_name = result.get("rel").get("name") - peer_id = result.get("peer").get("uuid") + for result in self.get_results(): + node_id = result.get("n_uuid") + rel_name = result.get("rel_name") + peer_id = result.get("peer_uuid") direction = str(result.get("direction")) direction_enum = { "inbound": RelationshipDirection.INBOUND, diff --git a/backend/tests/unit/core/test_node_query.py b/backend/tests/unit/core/test_node_query.py index 4fbf8b4820..06a387867c 100644 --- a/backend/tests/unit/core/test_node_query.py +++ b/backend/tests/unit/core/test_node_query.py @@ -385,6 +385,48 @@ async def test_query_NodeListGetRelationshipsQuery_hierarchical( ) assert parent_peer_ids == {europe_id} + # check with inbound only filter + query = await NodeListGetRelationshipsQuery.init( + db=db, + ids=node_ids, + branch=default_branch, + outbound_identifiers=[], + inbound_identifiers=["parent__child"], + bidirectional_identifiers=[], + ) + await query.execute(db=db) + grouped_peer_nodes = query.get_peers_group_by_node() + assert grouped_peer_nodes.has_node(paris_id) + child_peer_ids = grouped_peer_nodes.get_peer_ids( + node_id=paris_id, rel_name="parent__child", direction=RelationshipDirection.INBOUND + ) + assert child_peer_ids == {paris_r1_id, paris_r2_id} + parent_peer_ids = grouped_peer_nodes.get_peer_ids( + node_id=paris_id, rel_name="parent__child", direction=RelationshipDirection.OUTBOUND + ) + assert not parent_peer_ids + + # check with outbound only filter + query = await NodeListGetRelationshipsQuery.init( + db=db, + ids=node_ids, + branch=default_branch, + outbound_identifiers=["parent__child"], + inbound_identifiers=[], + bidirectional_identifiers=[], + ) + await query.execute(db=db) + grouped_peer_nodes = query.get_peers_group_by_node() + assert grouped_peer_nodes.has_node(paris_id) + child_peer_ids = grouped_peer_nodes.get_peer_ids( + node_id=paris_id, rel_name="parent__child", direction=RelationshipDirection.INBOUND + ) + assert not child_peer_ids + parent_peer_ids = grouped_peer_nodes.get_peer_ids( + node_id=paris_id, rel_name="parent__child", direction=RelationshipDirection.OUTBOUND + ) + assert parent_peer_ids == {europe_id} + async def test_query_NodeDeleteQuery( db: InfrahubDatabase, diff --git a/changelog/+node_list_get_relationship_query.fixed.md b/changelog/+node_list_get_relationship_query.fixed.md new file mode 100644 index 0000000000..f2966266a4 --- /dev/null +++ b/changelog/+node_list_get_relationship_query.fixed.md @@ -0,0 +1 @@ +Improve performance when retrieving nodes that have thousands of relationships \ No newline at end of file