sqry_cli/commands/
planner_query.rs1use crate::args::Cli;
17use crate::commands::graph::loader::{GraphLoadConfig, load_unified_graph_for_cli};
18use crate::index_discovery::find_nearest_index;
19use crate::output::OutputStreams;
20use anyhow::{Context, Result};
21use serde::Serialize;
22use std::path::PathBuf;
23use std::sync::Arc;
24
25use sqry_db::planner::{ParseError, execute_plan, parse_query};
26use sqry_db::queries::dispatch::make_query_db_cold;
27
28#[derive(Debug, Clone, Serialize)]
30pub struct PlanQueryHit {
31 pub name: String,
33 pub qualified_name: String,
35 pub kind: String,
37 pub file: String,
39 pub line: u32,
41 pub visibility: Option<String>,
43}
44
45pub fn run_planner_query(cli: &Cli, query: &str, path: Option<&str>, limit: usize) -> Result<()> {
54 let mut streams = OutputStreams::new();
55
56 let search_path = path.map_or_else(
57 || std::env::current_dir().unwrap_or_default(),
58 PathBuf::from,
59 );
60
61 let Some(location) = find_nearest_index(&search_path) else {
62 streams.write_diagnostic(
63 "No .sqry-index found. Run 'sqry index' first to build the graph index.",
64 )?;
65 return Ok(());
66 };
67
68 let config = GraphLoadConfig::default();
69 let graph = load_unified_graph_for_cli(&location.index_root, &config, cli)
70 .context("failed to load graph; run 'sqry index' to rebuild")?;
71
72 let plan = parse_query(query).map_err(format_parse_error)?;
73 let snapshot = Arc::new(graph.snapshot());
74
75 sqry_db::planner::cost_gate::check_plan(
83 &plan,
84 snapshot.nodes().len(),
85 &sqry_db::planner::cost_gate::PlannerCostGateConfig::default(),
86 )
87 .map_err(|e| anyhow::anyhow!("{e}"))?;
88
89 let db = make_query_db_cold(Arc::clone(&snapshot), &location.index_root);
90
91 let node_ids = execute_plan(&plan, &db);
92 let mut hits: Vec<PlanQueryHit> = Vec::with_capacity(node_ids.len().min(limit));
93
94 for node_id in node_ids.into_iter().take(limit) {
95 let Some(entry) = snapshot.nodes().get(node_id) else {
96 continue;
97 };
98 let strings = snapshot.strings();
99 let files = snapshot.files();
100 let name = strings
101 .resolve(entry.name)
102 .map(|s| s.to_string())
103 .unwrap_or_default();
104 let qualified_name = entry
105 .qualified_name
106 .and_then(|sid| strings.resolve(sid))
107 .map_or_else(|| name.clone(), |s| s.to_string());
108 let file = files
109 .resolve(entry.file)
110 .map(|p| p.display().to_string())
111 .unwrap_or_default();
112 let visibility = entry
113 .visibility
114 .and_then(|sid| strings.resolve(sid))
115 .map(|s| s.to_string());
116
117 hits.push(PlanQueryHit {
118 name,
119 qualified_name,
120 kind: entry.kind.as_str().to_string(),
121 file,
122 line: entry.start_line,
123 visibility,
124 });
125 }
126
127 if cli.json {
128 let payload =
129 serde_json::to_string_pretty(&hits).context("serializing plan-query hits as JSON")?;
130 streams.write_result(&payload)?;
131 } else {
132 for hit in &hits {
133 streams.write_result(&format!(
134 "{} {} {}:{}",
135 hit.kind, hit.qualified_name, hit.file, hit.line
136 ))?;
137 }
138 }
139
140 Ok(())
141}
142
143fn format_parse_error(err: ParseError) -> anyhow::Error {
146 anyhow::anyhow!("query parse error: {err}")
147}