use std::sync::Arc;
use sqry_core::graph::unified::edge::EdgeKind;
use sqry_core::graph::unified::edge::kind::TypeOfContext;
use sqry_core::graph::unified::materialize::find_nodes_by_name;
use sqry_core::graph::unified::node::NodeId;
use sqry_db::QueryDb;
use sqry_db::queries::RelationKey;
pub(crate) use sqry_db::queries::dispatch::{mcp_callees_query, mcp_callers_query};
#[allow(unused_imports)]
pub(crate) use sqry_db::queries::dispatch::{
mcp_exports_query, mcp_imports_query, mcp_references_query,
};
#[cfg(test)]
use sqry_db::queries::dispatch::make_query_db;
use crate::tools::RelationType;
#[must_use]
#[allow(dead_code)]
pub(crate) fn relation_endpoints_for_mcp(
db: &QueryDb,
relation: RelationType,
symbol: &str,
) -> Arc<Vec<NodeId>> {
let key = RelationKey::exact(symbol);
match relation {
RelationType::Callers => mcp_callers_query(db, &key),
RelationType::Callees => mcp_callees_query(db, &key),
RelationType::Imports => mcp_imports_query(db, &key),
RelationType::Exports => mcp_exports_query(db, &key),
RelationType::Returns => returns_targets_for_symbol(db, symbol),
}
}
#[must_use]
fn returns_targets_for_symbol(db: &QueryDb, symbol: &str) -> Arc<Vec<NodeId>> {
let snapshot = db.snapshot();
let candidates = find_nodes_by_name(snapshot, symbol);
let mut seen = std::collections::HashSet::new();
let mut out: Vec<NodeId> = Vec::new();
for candidate in candidates {
for edge in snapshot.edges().edges_from(candidate) {
if matches!(
edge.kind,
EdgeKind::TypeOf {
context: Some(TypeOfContext::Return),
..
}
) && seen.insert(edge.target)
{
out.push(edge.target);
}
}
}
Arc::new(out)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
use std::sync::Arc;
use sqry_core::graph::unified::concurrent::{CodeGraph, GraphSnapshot};
use sqry_core::graph::unified::edge::EdgeKind;
use sqry_core::graph::unified::edge::kind::TypeOfContext;
use sqry_core::graph::unified::node::NodeKind;
use sqry_core::graph::unified::storage::NodeEntry;
fn build_caller_graph() -> (Arc<GraphSnapshot>, NodeId, NodeId, NodeId) {
let mut graph = CodeGraph::new();
let file_id = graph.files_mut().register(Path::new("lib.rs")).unwrap();
let main_name = graph.strings_mut().intern("main").unwrap();
let helper_name = graph.strings_mut().intern("helper").unwrap();
let isolated_name = graph.strings_mut().intern("isolated").unwrap();
let main_id = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Function, main_name, file_id)
.with_qualified_name(main_name),
)
.unwrap();
let helper_id = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Function, helper_name, file_id)
.with_qualified_name(helper_name),
)
.unwrap();
let isolated_id = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Function, isolated_name, file_id)
.with_qualified_name(isolated_name),
)
.unwrap();
graph.edges_mut().add_edge(
main_id,
helper_id,
EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
file_id,
);
(Arc::new(graph.snapshot()), main_id, helper_id, isolated_id)
}
#[test]
fn mcp_dispatch_routes_relation_type_to_correct_query() {
let (snapshot, main_id, helper_id, _isolated_id) = build_caller_graph();
let db = make_query_db(snapshot);
let callers_set = relation_endpoints_for_mcp(&db, RelationType::Callers, "helper");
assert!(callers_set.contains(&main_id));
let callees_set = relation_endpoints_for_mcp(&db, RelationType::Callees, "main");
assert!(callees_set.contains(&helper_id));
let returns_set = relation_endpoints_for_mcp(&db, RelationType::Returns, "anything");
assert!(
returns_set.is_empty(),
"Returns dispatch must yield empty when no TypeOf{{Return}} edges exist"
);
}
fn build_returns_graph() -> (Arc<GraphSnapshot>, NodeId, NodeId) {
let mut graph = CodeGraph::new();
let file_id = graph.files_mut().register(Path::new("lib.rs")).unwrap();
let parse_name = graph.strings_mut().intern("parseConfig").unwrap();
let error_name = graph.strings_mut().intern("error_type").unwrap();
let parse_id = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Function, parse_name, file_id)
.with_qualified_name(parse_name),
)
.unwrap();
let error_id = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Type, error_name, file_id).with_qualified_name(error_name),
)
.unwrap();
graph.edges_mut().add_edge(
parse_id,
error_id,
EdgeKind::TypeOf {
context: Some(TypeOfContext::Return),
index: None,
name: None,
},
file_id,
);
graph.rebuild_indices();
(Arc::new(graph.snapshot()), parse_id, error_id)
}
#[test]
fn mcp_dispatch_returns_walks_typeof_return_edges() {
let (snapshot, _parse_id, error_id) = build_returns_graph();
let db = make_query_db(snapshot);
let returns_set = relation_endpoints_for_mcp(&db, RelationType::Returns, "parseConfig");
assert!(
returns_set.contains(&error_id),
"Returns dispatch must surface the target of the TypeOf{{Return}} edge \
(got {returns_set:?}, expected to contain {error_id:?})",
);
}
#[test]
fn mcp_dispatch_returns_empty_for_unknown_symbol() {
let (snapshot, _parse_id, _error_id) = build_returns_graph();
let db = make_query_db(snapshot);
let returns_set =
relation_endpoints_for_mcp(&db, RelationType::Returns, "no_such_function");
assert!(
returns_set.is_empty(),
"Unknown symbols must dispatch to an empty Returns set, not panic or return all targets"
);
}
}