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 let db = make_query_db_cold(Arc::clone(&snapshot), &location.index_root);
75
76 let node_ids = execute_plan(&plan, &db);
77 let mut hits: Vec<PlanQueryHit> = Vec::with_capacity(node_ids.len().min(limit));
78
79 for node_id in node_ids.into_iter().take(limit) {
80 let Some(entry) = snapshot.nodes().get(node_id) else {
81 continue;
82 };
83 let strings = snapshot.strings();
84 let files = snapshot.files();
85 let name = strings
86 .resolve(entry.name)
87 .map(|s| s.to_string())
88 .unwrap_or_default();
89 let qualified_name = entry
90 .qualified_name
91 .and_then(|sid| strings.resolve(sid))
92 .map_or_else(|| name.clone(), |s| s.to_string());
93 let file = files
94 .resolve(entry.file)
95 .map(|p| p.display().to_string())
96 .unwrap_or_default();
97 let visibility = entry
98 .visibility
99 .and_then(|sid| strings.resolve(sid))
100 .map(|s| s.to_string());
101
102 hits.push(PlanQueryHit {
103 name,
104 qualified_name,
105 kind: entry.kind.as_str().to_string(),
106 file,
107 line: entry.start_line,
108 visibility,
109 });
110 }
111
112 if cli.json {
113 let payload =
114 serde_json::to_string_pretty(&hits).context("serializing plan-query hits as JSON")?;
115 streams.write_result(&payload)?;
116 } else {
117 for hit in &hits {
118 streams.write_result(&format!(
119 "{} {} {}:{}",
120 hit.kind, hit.qualified_name, hit.file, hit.line
121 ))?;
122 }
123 }
124
125 Ok(())
126}
127
128fn format_parse_error(err: ParseError) -> anyhow::Error {
131 anyhow::anyhow!("query parse error: {err}")
132}