use anyhow::{Context, Result};
use magellan::backend_router::{BackendType, MagellanBackend};
use magellan::common::{
detect_language_from_path, format_symbol_kind, parse_symbol_kind, resolve_path,
};
use magellan::output::rich::{SpanChecksums, SpanContext};
use magellan::output::{
output_json, CalleeInfo, CallerInfo, JsonResponse, OutputFormat, QueryResponse, Span,
SymbolMatch,
};
use magellan::{CodeGraph, SymbolFact};
use std::path::PathBuf;
const QUERY_EXPLAIN_TEXT: &str = r#"Query Selector Cheatsheet
--------------------------------
Selectors:
Required selectors:
--file <path> Absolute or root-relative path to inspect.
Optional filters:
--kind <kind> function|method|struct|trait|enum|mod|type_alias|union|namespace.
--symbol <name> Limit output to a specific symbol (case-sensitive).
--show-extent With --symbol, print byte + line/column ranges.
Related helpers:
magellan refs --name <symbol> --path <file> Show incoming/outgoing references.
magellan find --name <symbol> Locate symbol across files.
magellan find --list-glob \"test_*\" Preview glob sets before bulk edits.
Examples:
magellan query --db mag.db --file src/main.rs --kind function
magellan query --db mag.db --file src/lib.rs --symbol main --show-extent
magellan find --db mag.db --list-glob \"handler_*\""#;
pub fn run_query(
db_path: PathBuf,
file_path: Option<PathBuf>,
root: Option<PathBuf>,
kind_str: Option<String>,
explain: bool,
symbol: Option<String>,
show_extent: bool,
output_format: OutputFormat,
with_context: bool,
with_callers: bool,
with_callees: bool,
with_semantics: bool,
with_checksums: bool,
context_lines: usize,
) -> Result<()> {
let mut args = vec!["query".to_string()];
if let Some(ref fp) = file_path {
args.push("--file".to_string());
args.push(fp.to_string_lossy().to_string());
}
if let Some(ref root_path) = root {
args.push("--root".to_string());
args.push(root_path.to_string_lossy().to_string());
}
if let Some(ref kind) = kind_str {
args.push("--kind".to_string());
args.push(kind.clone());
}
if explain {
args.push("--explain".to_string());
}
if let Some(ref sym) = symbol {
args.push("--symbol".to_string());
args.push(sym.clone());
}
if show_extent {
args.push("--show-extent".to_string());
}
if MagellanBackend::detect_type(&db_path) == BackendType::Geometric {
return run_query_geometric(
&db_path,
file_path.as_deref().and_then(|p| p.to_str()),
symbol.as_deref(),
explain,
show_extent,
);
}
let mut graph = CodeGraph::open(&db_path)?;
let exec_id = magellan::output::generate_execution_id();
let root_str = root.as_ref().map(|p| p.to_string_lossy().to_string());
let db_path_str = db_path.to_string_lossy().to_string();
graph.execution_log().start_execution(
&exec_id,
env!("CARGO_PKG_VERSION"),
&args,
root_str.as_deref(),
&db_path_str,
)?;
if explain {
println!("{}", QUERY_EXPLAIN_TEXT);
let _ = graph
.execution_log()
.finish_execution(&exec_id, "success", None, 0, 0, 0);
return Ok(());
}
if show_extent && symbol.is_none() {
let err_msg = "--show-extent requires --symbol <name>".to_string();
let _ = graph
.execution_log()
.finish_execution(&exec_id, "error", Some(&err_msg), 0, 0, 0);
anyhow::bail!(err_msg);
}
let kind_filter = match kind_str {
Some(ref s) => match parse_symbol_kind(s) {
Some(k) => Some(k),
None => {
let err_msg = format!("Unknown symbol kind: '{}'. Valid kinds: function, method, class, interface, enum, module, union, namespace, typealias", s);
let _ = graph.execution_log().finish_execution(
&exec_id,
"error",
Some(&err_msg),
0,
0,
0,
);
anyhow::bail!(err_msg);
}
},
None => None,
};
let file_path = match file_path {
Some(fp) => fp,
None => {
let err_msg = "--file is required unless --explain is used".to_string();
let _ =
graph
.execution_log()
.finish_execution(&exec_id, "error", Some(&err_msg), 0, 0, 0);
anyhow::bail!(err_msg);
}
};
let path_str = resolve_path(&file_path, &root);
if output_format == OutputFormat::Json || output_format == OutputFormat::Pretty {
let mut symbols_with_ids =
magellan::graph::query::symbol_nodes_in_file_with_ids(&mut graph, &path_str)?;
if let Some(ref filter_kind) = kind_filter {
symbols_with_ids.retain(|(_, fact, _)| fact.kind == *filter_kind);
}
if let Some(ref symbol_name) = symbol {
symbols_with_ids
.retain(|(_, fact, _)| fact.name.as_deref() == Some(symbol_name.as_str()));
}
let symbols_with_ids: Vec<(SymbolFact, Option<String>)> = symbols_with_ids
.into_iter()
.map(|(_, fact, symbol_id)| (fact, symbol_id))
.collect();
let _ = graph
.execution_log()
.finish_execution(&exec_id, "success", None, 0, 0, 0);
return output_json_mode(
&path_str,
symbols_with_ids,
kind_str,
show_extent,
&symbol,
&mut graph,
&exec_id,
output_format,
with_context,
with_callers,
with_callees,
with_semantics,
with_checksums,
context_lines,
);
}
let mut symbols = graph.symbols_in_file_with_kind(&path_str, kind_filter)?;
if let Some(ref symbol_name) = symbol {
symbols.retain(|s| s.name.as_deref() == Some(symbol_name.as_str()));
}
println!("{}:", path_str);
if symbols.is_empty() {
println!(" (no symbols found)");
match symbol {
Some(ref sym) => println!(
" Hint: verify the symbol name or run `magellan find --list-glob \"{}\"`.",
sym
),
None => println!(" Hint: run `magellan query --explain` for selector syntax."),
}
let _ = graph
.execution_log()
.finish_execution(&exec_id, "success", None, 0, 0, 0);
return Ok(());
}
for symbol in &symbols {
let kind_str = format_symbol_kind(&symbol.kind);
let name = symbol.name.as_deref().unwrap_or("(unnamed)");
println!(
" Line {:4}: {:12} {:<} [{}]",
symbol.start_line, kind_str, name, symbol.kind_normalized
);
if with_callers {
let symbol_path = symbol.file_path.to_string_lossy().to_string();
if let Ok(callers) = graph.callers_of_symbol(&symbol_path, name) {
if !callers.is_empty() {
println!(" Called from:");
for caller in &callers {
println!(
" {} at {}:{}",
caller.caller,
caller.file_path.display(),
caller.start_line
);
}
}
}
}
if with_callees {
let symbol_path = symbol.file_path.to_string_lossy().to_string();
if let Ok(callees) = graph.calls_from_symbol(&symbol_path, name) {
if !callees.is_empty() {
println!(" Calls:");
for callee in &callees {
println!(" {} at {}", callee.callee, callee.file_path.display());
}
}
}
}
}
if show_extent {
if let Some(ref symbol_name) = symbol {
let mut extents = graph.symbol_extents(&path_str, symbol_name)?;
if extents.is_empty() {
println!(" (no extent info found for '{}')", symbol_name);
let _ = graph
.execution_log()
.finish_execution(&exec_id, "success", None, 0, 0, 0);
return Ok(());
}
println!();
println!("Symbol Extents for '{}':", symbol_name);
extents.sort_by(|(_, a), (_, b)| {
a.start_line
.cmp(&b.start_line)
.then_with(|| a.start_col.cmp(&b.start_col))
});
for (node_id, fact) in extents {
print_extent_block(node_id, &fact);
}
}
}
let _ = graph
.execution_log()
.finish_execution(&exec_id, "success", None, 0, 0, 0);
Ok(())
}
fn output_json_mode(
path_str: &str,
mut symbols_with_ids: Vec<(SymbolFact, Option<String>)>,
kind_str: Option<String>,
_show_extent: bool,
_symbol: &Option<String>,
graph: &mut CodeGraph,
exec_id: &str,
output_format: OutputFormat,
with_context: bool,
with_callers: bool,
with_callees: bool,
with_semantics: bool,
with_checksums: bool,
context_lines: usize,
) -> Result<()> {
symbols_with_ids.sort_by(|(a, _), (b, _)| {
a.file_path
.cmp(&b.file_path)
.then_with(|| a.start_line.cmp(&b.start_line))
.then_with(|| a.start_col.cmp(&b.start_col))
.then_with(|| a.name.as_deref().cmp(&b.name.as_deref()))
});
let symbol_matches: Vec<SymbolMatch> = symbols_with_ids
.into_iter()
.map(|(s, symbol_id)| {
let file_path = s.file_path.to_string_lossy().to_string();
let mut span = Span::new(
file_path.clone(),
s.byte_start,
s.byte_end,
s.start_line,
s.start_col,
s.end_line,
s.end_col,
);
if with_context {
if let Some(context) =
SpanContext::extract(&file_path, s.start_line, s.end_line, context_lines)
{
span = span.with_context(context);
}
}
if with_semantics {
let language = detect_language_from_path(&file_path);
span = span.with_semantics_from(s.kind_normalized.clone(), language);
}
if with_checksums {
let checksums = SpanChecksums::compute(&file_path, s.byte_start, s.byte_end);
span = span.with_checksums(checksums);
}
let symbol_name = s.name.clone().unwrap_or_else(|| "(unnamed)".to_string());
let callers_info = if with_callers {
if let Ok(call_facts) = graph.callers_of_symbol(&file_path, &symbol_name) {
let mut callers: Vec<CallerInfo> = call_facts
.into_iter()
.map(|call| CallerInfo {
name: call.caller,
file_path: call.file_path.to_string_lossy().to_string(),
line: call.start_line,
column: call.start_col,
})
.collect();
callers.sort_by(|a, b| {
a.file_path
.cmp(&b.file_path)
.then_with(|| a.line.cmp(&b.line))
});
Some(callers)
} else {
None
}
} else {
None
};
let callees_info = if with_callees {
if let Ok(call_facts) = graph.calls_from_symbol(&file_path, &symbol_name) {
let mut callees: Vec<CalleeInfo> = call_facts
.into_iter()
.map(|call| CalleeInfo {
name: call.callee,
file_path: call.file_path.to_string_lossy().to_string(),
})
.collect();
callees.sort_by(|a, b| {
a.file_path
.cmp(&b.file_path)
.then_with(|| a.name.cmp(&b.name))
});
Some(callees)
} else {
None
}
} else {
None
};
SymbolMatch::new(
symbol_name,
s.kind_normalized,
span,
None, symbol_id, )
.with_callers_and_callees(callers_info, callees_info)
})
.collect();
let response = QueryResponse {
symbols: symbol_matches,
file_path: path_str.to_string(),
kind_filter: kind_str,
};
let json_response = JsonResponse::new(response, exec_id);
output_json(&json_response, output_format)?;
Ok(())
}
fn print_extent_block(node_id: i64, symbol: &magellan::SymbolFact) {
let name = symbol.name.as_deref().unwrap_or("(unnamed)");
println!(" Node ID: {}", node_id);
println!(
" {} [{}] at {}",
name,
symbol.kind_normalized,
symbol.file_path.to_string_lossy()
);
println!(" Byte Range: {}..{}", symbol.byte_start, symbol.byte_end);
println!(
" Line Range: {}:{} -> {}:{}",
symbol.start_line, symbol.start_col, symbol.end_line, symbol.end_col
);
}
fn run_query_geometric(
db_path: &std::path::Path,
file_path: Option<&str>,
symbol: Option<&str>,
explain: bool,
show_extent: bool,
) -> Result<()> {
let backend = MagellanBackend::open(db_path)?;
if explain {
println!("{}", QUERY_EXPLAIN_TEXT);
return Ok(());
}
if show_extent && symbol.is_none() {
anyhow::bail!("--show-extent requires --symbol <name>");
}
let stats = backend.get_stats()?;
println!("Geometric Database Statistics:");
println!(" Symbols: {}", stats.symbol_count);
println!(" CFG Blocks: {}", stats.cfg_block_count);
if let Some(sym_name) = symbol {
match backend.find_symbol_by_fqn(sym_name) {
Ok(Some(info)) => {
println!("\nFound symbol: {}", info.fqn);
println!(" Name: {}", info.name);
println!(" Kind: {:?}", info.kind);
println!(" File: {}", info.file_path);
println!(
" Location: Line {}, Column {}",
info.start_line, info.start_col
);
Ok(())
}
Ok(None) => {
println!("\nSymbol '{}' not found", sym_name);
Ok(())
}
Err(e) => {
eprintln!("Error: {}", e);
Err(e)
}
}?;
} else if let Some(fp) = file_path {
println!("\nQuerying file: {}", fp);
match backend.symbols_in_file(fp) {
Ok(symbols) => {
if symbols.is_empty() {
println!("No symbols found in file");
} else {
println!("Found {} symbol(s):\n", symbols.len());
for (i, info) in symbols.iter().enumerate() {
println!(" [{}] {}", i + 1, info.fqn);
println!(" Kind: {:?}", info.kind);
println!(" Lines: {}-{}", info.start_line, info.end_line);
println!();
}
}
Ok(())
}
Err(e) => Err(e).context("Error querying symbols in file"),
}?;
} else {
println!("\nUse --symbol <fqn> to query specific symbols");
println!("Use --file <path> to query by file");
}
Ok(())
}