use serde_json::json;
use std::path::Path;
use super::helpers::{log_execution_error, write_stdout_bytes, write_stdout_line};
#[allow(
clippy::too_many_arguments,
reason = "CLI handler aggregates clap-parsed flags"
)]
#[allow(unused_variables, reason = "stub args reserved for future expansion")]
pub(crate) fn execute_query(
db_path: &Path,
labels: &[String],
file_filter: Option<&str>,
context_before: usize,
context_after: usize,
context_both: usize,
list: bool,
count: bool,
show_code: bool,
relationships: bool,
expand: bool,
expand_level: usize,
_json_output: bool,
) -> Result<splice::cli::CliSuccessPayload, splice::SpliceError> {
use splice::execution::log;
use splice::graph::magellan_integration::MagellanIntegration;
let (ctx_before, ctx_after) =
splice::resolve_context_counts(context_before, context_after, context_both);
let start = std::time::Instant::now();
let command_line = std::env::args().collect::<Vec<_>>().join(" ");
let integration = MagellanIntegration::open(db_path)?;
#[cfg(feature = "sqlite")]
if list {
let all_labels = integration.get_all_labels()?;
write_stdout_line(&format!("{} labels in use:", all_labels.len()))?;
for label in &all_labels {
let count = integration.count_by_label(label)?;
write_stdout_line(&format!(" {} ({})", label, count))?;
}
let duration_ms = start.elapsed().as_millis() as i64;
let label_count = all_labels.len();
let message = format!("Listed {} labels", label_count);
let parameters = serde_json::json!({
"db": db_path.to_string_lossy(),
"list": true,
"label_count": label_count,
});
if let Err(e) = log::record_execution_with_params(
&splice::output::OperationResult::new("query".to_string()).success(message.clone()),
duration_ms,
Some(command_line.clone()),
parameters,
) {
log_execution_error("query", &e);
}
return Ok(splice::cli::CliSuccessPayload::message_only(message));
}
#[cfg(not(feature = "sqlite"))]
if list {
return Err(splice::SpliceError::Other(
"The --list flag requires SQLite backend. \
Use default SQLite backend: `cargo build` (no --features flag)"
.to_string(),
));
}
#[cfg(feature = "sqlite")]
if count {
if labels.is_empty() {
return Err(splice::SpliceError::Other(
"--count requires at least one --label".to_string(),
));
}
let mut counts = serde_json::Map::new();
for label in labels {
let entity_count = integration.count_by_label(label)?;
counts.insert(label.clone(), json!(entity_count));
}
let duration_ms = start.elapsed().as_millis() as i64;
let labels_count = labels.len();
let message = format!("Counted entities for {} label(s)", labels_count);
let parameters = serde_json::json!({
"db": db_path.to_string_lossy(),
"count": true,
"labels": labels,
});
if let Err(e) = log::record_execution_with_params(
&splice::output::OperationResult::new("query".to_string()).success(message.clone()),
duration_ms,
Some(command_line.clone()),
parameters,
) {
log_execution_error("query", &e);
}
return Ok(splice::cli::CliSuccessPayload::with_data(
message,
json!(counts),
));
}
#[cfg(not(feature = "sqlite"))]
if count {
return Err(splice::SpliceError::Other(
"The --count flag requires SQLite backend. \
Use default SQLite backend: `cargo build` (no --features flag)"
.to_string(),
));
}
#[cfg(not(feature = "sqlite"))]
return Err(splice::SpliceError::Other(
"Label queries require SQLite backend. \
Use default SQLite backend: `cargo build` (no --features flag)"
.to_string(),
));
#[cfg(feature = "sqlite")]
if labels.is_empty() {
return Err(splice::SpliceError::Other(
"No labels specified. Use --label <LABEL> or --list to see all labels".to_string(),
));
}
#[cfg(feature = "sqlite")]
let labels_ref: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
#[cfg(feature = "sqlite")]
let mut results = integration.query_by_labels(&labels_ref)?;
#[cfg(feature = "sqlite")]
if let Some(file_pattern) = file_filter {
results.retain(|r| r.file_path.contains(file_pattern));
if results.is_empty() {
return Err(splice::SpliceError::Other(format!(
"No symbols found with labels {:?} in file pattern '{}'",
labels, file_pattern
)));
}
}
#[cfg(feature = "sqlite")]
results.sort_by(|a, b| {
a.file_path
.cmp(&b.file_path)
.then_with(|| a.byte_start.cmp(&b.byte_start))
});
#[cfg(feature = "sqlite")]
if results.is_empty() {
if labels.len() == 1 {
write_stdout_line(&format!("No symbols found with label '{}'", labels[0]))?;
} else {
write_stdout_line(&format!(
"No symbols found with labels: {}",
labels.join(", ")
))?;
}
let duration_ms = start.elapsed().as_millis() as i64;
let message = "No symbols found".to_string();
let parameters = serde_json::json!({
"db": db_path.to_string_lossy(),
"labels": labels,
"results_count": 0,
});
if let Err(e) = log::record_execution_with_params(
&splice::output::OperationResult::new("query".to_string()).success(message.clone()),
duration_ms,
Some(command_line.clone()),
parameters,
) {
log_execution_error("query", &e);
}
return Ok(splice::cli::CliSuccessPayload::message_only(message));
}
#[cfg(feature = "sqlite")]
if _json_output {
use splice::action::{suggest_action, ActionType, Confidence};
use splice::checksum;
use splice::context;
use splice::hints::{derive_tool_hints, ToolHintOperation};
use splice::ingest::detect as ingest_detect;
use splice::ingest::semantic_kind::SemanticKind;
use splice::output::{OperationData, OperationResult, QueryResult, SpanResult};
let code_graph = if relationships {
Some(splice::graph::CodeGraph::open(db_path)?)
} else {
None
};
let mut symbols: Vec<SpanResult> = Vec::new();
for r in &results {
let path = std::path::Path::new(&r.file_path);
let (expanded_start, expanded_end) = if expand && expand_level > 0 {
use splice::expand::expand_to_body_with_docs;
use splice::ingest::detect as ingest_detect;
use splice::symbol::Language;
let lang = ingest_detect::detect_language(path);
match lang {
Some(detected_lang) => {
let language = match detected_lang {
ingest_detect::Language::Rust => Language::Rust,
ingest_detect::Language::Python => Language::Python,
ingest_detect::Language::C => Language::C,
ingest_detect::Language::Cpp => Language::Cpp,
ingest_detect::Language::Java => Language::Java,
ingest_detect::Language::JavaScript => Language::JavaScript,
ingest_detect::Language::TypeScript => Language::TypeScript,
};
match expand_to_body_with_docs(path, r.byte_start, language) {
Ok((exp_start, exp_end)) => (exp_start, exp_end),
Err(_) => (r.byte_start, r.byte_end), }
}
None => (r.byte_start, r.byte_end), }
} else {
(r.byte_start, r.byte_end)
};
let (span_start, span_end) = (expanded_start, expanded_end);
let mut span = SpanResult::from_byte_span(r.file_path.clone(), span_start, span_end)
.with_symbol(r.name.clone(), r.kind.clone());
if ctx_before > 0 || ctx_after > 0 {
if let Ok(ctx) = context::extract_context_asymmetric(
path, span_start, span_end, ctx_before, ctx_after,
) {
span = span.with_context(ctx);
}
}
let (sem_kind, is_public) = if let Some(lang) = ingest_detect::detect_language(path) {
let sem_kind = match r.kind.as_str() {
"fn" | "function" | "method" => SemanticKind::Function,
"struct" | "class" | "type" => SemanticKind::Type,
"trait" | "interface" => SemanticKind::Trait,
"enum" => SemanticKind::Enum,
"module" => SemanticKind::Module,
"const" | "static" => SemanticKind::Constant,
_ => SemanticKind::Unknown,
};
let is_public = matches!(
sem_kind,
SemanticKind::Function
| SemanticKind::Type
| SemanticKind::Trait
| SemanticKind::Enum
);
span = span.with_semantic_info(sem_kind.as_str(), lang.as_str());
(sem_kind, is_public)
} else {
(SemanticKind::Unknown, false)
};
let hints = derive_tool_hints(sem_kind, is_public, ToolHintOperation::Query);
span = span.with_tool_hints(hints);
let action = suggest_action(
ActionType::Query,
&r.name,
&r.kind,
&r.file_path,
Confidence::High,
);
span = span.with_suggested_action(action);
if let Ok(cs) = checksum::checksum_span(path, span_start, span_end) {
span = span.with_checksum_before(cs.value);
}
if let Ok(file_cs) = checksum::checksum_file(path) {
span = span.with_file_checksum_before(file_cs.value);
}
if relationships {
if let Some(ref graph) = code_graph {
use splice::relationships::{
get_callees, get_callers, get_exports, get_imports, RelationshipCache,
Relationships,
};
use sqlitegraph::NodeId;
let mut cache = RelationshipCache::new();
let node_id = NodeId::from(r.entity_id);
let callers = get_callers(graph, node_id, &mut cache).unwrap_or_default();
let callees = get_callees(graph, node_id, &mut cache).unwrap_or_default();
let imports = get_imports(graph, path, &mut cache).unwrap_or_default();
let exports = get_exports(graph, path, &mut cache).unwrap_or_default();
let rels = Relationships {
callers,
callees,
imports,
exports,
cycle_detected: false,
error_code: None,
};
span = span.with_relationships(rels);
}
}
symbols.push(span);
}
symbols.sort();
let query_result = QueryResult {
labels: labels.to_vec(),
count: symbols.len(),
symbols,
total_count: None,
offset: None,
limit: None,
max_symbols: None,
max_bytes: None,
next_offset: None,
partial: None,
truncation_reasons: None,
};
let results_count = query_result.count;
let result = OperationResult::new("query".to_string())
.success(format!("Found {} symbols", results_count))
.with_result(OperationData::Query(query_result));
println!(
"{}",
serde_json::to_string_pretty(&result)
.expect("invariant: serde_json serialization never fails on serializable types")
);
let duration_ms = start.elapsed().as_millis() as i64;
let parameters = serde_json::json!({
"db": db_path.to_string_lossy(),
"labels": labels,
"show_code": show_code,
"results_count": results_count,
});
if let Err(e) =
log::record_execution_with_params(&result, duration_ms, Some(command_line), parameters)
{
log_execution_error("query", &e);
}
return Ok(
splice::cli::CliSuccessPayload::message_only("OK".to_string()).already_emitted(),
);
}
#[cfg(feature = "sqlite")]
struct ExpandedResult {
result: splice::graph::magellan_integration::SymbolInfo,
expanded_start: usize,
expanded_end: usize,
}
#[cfg(feature = "sqlite")]
let expanded_results: Vec<ExpandedResult> = results
.iter()
.map(|r| {
let (exp_start, exp_end) = if expand && expand_level > 0 {
use splice::expand::expand_to_body_with_docs;
use splice::ingest::detect as ingest_detect;
use splice::symbol::Language;
let path = std::path::Path::new(&r.file_path);
let lang = ingest_detect::detect_language(path);
match lang {
Some(detected_lang) => {
let language = match detected_lang {
ingest_detect::Language::Rust => Language::Rust,
ingest_detect::Language::Python => Language::Python,
ingest_detect::Language::C => Language::C,
ingest_detect::Language::Cpp => Language::Cpp,
ingest_detect::Language::Java => Language::Java,
ingest_detect::Language::JavaScript => Language::JavaScript,
ingest_detect::Language::TypeScript => Language::TypeScript,
};
match expand_to_body_with_docs(path, r.byte_start, language) {
Ok((start, end)) => (start, end),
Err(_) => (r.byte_start, r.byte_end),
}
}
None => (r.byte_start, r.byte_end),
}
} else {
(r.byte_start, r.byte_end)
};
ExpandedResult {
result: r.clone(),
expanded_start: exp_start,
expanded_end: exp_end,
}
})
.collect();
#[cfg(feature = "sqlite")]
let symbols_data: Vec<serde_json::Value> = expanded_results
.iter()
.map(|er| {
let mut data = json!({
"entity_id": er.result.entity_id,
"name": er.result.name,
"file_path": er.result.file_path,
"kind": er.result.kind,
"byte_start": er.result.byte_start,
"byte_end": er.result.byte_end,
});
if expand
&& expand_level > 0
&& (er.expanded_start != er.result.byte_start
|| er.expanded_end != er.result.byte_end)
{
data["expanded_byte_start"] = json!(er.expanded_start);
data["expanded_byte_end"] = json!(er.expanded_end);
}
data
})
.collect();
#[cfg(feature = "sqlite")]
if labels.len() == 1 {
write_stdout_line(&format!(
"{} symbols with label '{}':",
expanded_results.len(),
labels[0]
))?;
} else {
write_stdout_line(&format!(
"{} symbols with labels [{}]:",
expanded_results.len(),
labels.join(", ")
))?;
}
#[cfg(feature = "sqlite")]
for er in &expanded_results {
write_stdout_line("")?;
write_stdout_line(&format!(
" {} ({}) in {} [{}-{}]",
er.result.name,
er.result.kind,
er.result.file_path,
er.result.byte_start,
er.result.byte_end
))?;
if !show_code && (ctx_before > 0 || ctx_after > 0) {
use splice::context;
let path = std::path::Path::new(&er.result.file_path);
if let Ok(ctx) = context::extract_context_asymmetric(
path,
er.expanded_start,
er.expanded_end,
ctx_before,
ctx_after,
) {
if !ctx.before.is_empty() {
write_stdout_line(&format!(" Context ({} lines before):", ctx.before.len()))?;
for line in &ctx.before {
write_stdout_line(&format!(" {}", line))?;
}
}
if !ctx.after.is_empty() {
write_stdout_line(&format!(" Context ({} lines after):", ctx.after.len()))?;
for line in &ctx.after {
write_stdout_line(&format!(" {}", line))?;
}
}
}
}
if show_code {
let path = std::path::Path::new(&er.result.file_path);
if let Ok(Some(code)) =
integration.get_code_chunk(path, er.expanded_start, er.expanded_end)
{
if ctx_before > 0 || ctx_after > 0 {
use splice::context;
if let Ok(ctx) = context::extract_context_asymmetric(
path,
er.expanded_start,
er.expanded_end,
ctx_before,
ctx_after,
) {
if !ctx.before.is_empty() {
write_stdout_line(&format!(
" Context ({} lines before):",
ctx.before.len()
))?;
for line in &ctx.before {
write_stdout_line(&format!(" {}", line))?;
}
}
}
}
write_stdout_line(" Code:")?;
for line in code.lines() {
write_stdout_line(&format!(" {}", line))?;
}
if ctx_before > 0 || ctx_after > 0 {
use splice::context;
if let Ok(ctx) = context::extract_context_asymmetric(
path,
er.expanded_start,
er.expanded_end,
ctx_before,
ctx_after,
) {
if !ctx.after.is_empty() {
write_stdout_line(&format!(
" Context ({} lines after):",
ctx.after.len()
))?;
for line in &ctx.after {
write_stdout_line(&format!(" {}", line))?;
}
}
}
}
}
}
}
#[cfg(feature = "sqlite")]
let duration_ms = start.elapsed().as_millis() as i64;
#[cfg(feature = "sqlite")]
let results_count = results.len();
#[cfg(feature = "sqlite")]
let message = format!("Found {} symbols", results_count);
#[cfg(feature = "sqlite")]
let parameters = serde_json::json!({
"db": db_path.to_string_lossy(),
"labels": labels,
"show_code": show_code,
"results_count": results_count,
});
#[cfg(feature = "sqlite")]
if let Err(e) = log::record_execution_with_params(
&splice::output::OperationResult::new("query".to_string()).success(message.clone()),
duration_ms,
Some(command_line.clone()),
parameters,
) {
log_execution_error("query", &e);
}
#[cfg(feature = "sqlite")]
{
Ok(splice::cli::CliSuccessPayload::with_data(
message,
json!(symbols_data),
))
}
#[cfg(not(feature = "sqlite"))]
{
Ok(splice::cli::CliSuccessPayload::message_only(
"Label queries require SQLite backend".to_string(),
))
}
}
#[allow(
clippy::too_many_arguments,
reason = "CLI handler aggregates clap-parsed flags"
)]
#[allow(unused_variables, reason = "stub args reserved for future expansion")]
pub(crate) fn execute_get(
db_path: &Path,
file_path: &Path,
start: usize,
end: usize,
context_before: usize,
context_after: usize,
context_both: usize,
relationships: bool,
expand: bool,
expand_level: usize,
_json_output: bool,
) -> Result<splice::cli::CliSuccessPayload, splice::SpliceError> {
use splice::graph::magellan_integration::MagellanIntegration;
let (ctx_before, ctx_after) =
splice::resolve_context_counts(context_before, context_after, context_both);
let (expanded_start, expanded_end) = if expand && expand_level > 0 {
use splice::expand::expand_to_body_with_docs;
use splice::ingest::detect as ingest_detect;
use splice::symbol::Language;
let lang = ingest_detect::detect_language(file_path);
match lang {
Some(detected_lang) => {
let language = match detected_lang {
ingest_detect::Language::Rust => Language::Rust,
ingest_detect::Language::Python => Language::Python,
ingest_detect::Language::C => Language::C,
ingest_detect::Language::Cpp => Language::Cpp,
ingest_detect::Language::Java => Language::Java,
ingest_detect::Language::JavaScript => Language::JavaScript,
ingest_detect::Language::TypeScript => Language::TypeScript,
};
match expand_to_body_with_docs(file_path, start, language) {
Ok((exp_start, exp_end)) => (exp_start, exp_end),
Err(_) => (start, end), }
}
None => (start, end), }
} else {
(start, end)
};
let integration = MagellanIntegration::open(db_path)?;
let code = integration.get_code_chunk(file_path, expanded_start, expanded_end)?;
match code {
Some(content) => {
if _json_output {
use splice::action::{suggest_action, ActionType, Confidence};
use splice::checksum;
use splice::context;
use splice::hints::{derive_tool_hints, ToolHintOperation};
use splice::ingest::detect as ingest_detect;
use splice::ingest::semantic_kind::SemanticKind;
use splice::output::{OperationData, OperationResult, SpanResult};
let (span_start, span_end) = (expanded_start, expanded_end);
let mut span = SpanResult::from_byte_span(
file_path.to_string_lossy().to_string(),
span_start,
span_end,
);
if ctx_before > 0 || ctx_after > 0 {
if let Ok(ctx) = context::extract_context_asymmetric(
file_path, span_start, span_end, ctx_before, ctx_after,
) {
span = span.with_context(ctx);
}
}
let (sem_kind, is_public) =
if let Some(lang) = ingest_detect::detect_language(file_path) {
let sem_kind = SemanticKind::Function;
let is_public = true;
span = span.with_semantic_info(sem_kind.as_str(), lang.as_str());
(sem_kind, is_public)
} else {
(SemanticKind::Unknown, false)
};
let hints = derive_tool_hints(sem_kind, is_public, ToolHintOperation::Get);
span = span.with_tool_hints(hints);
let action = suggest_action(
ActionType::Read,
"code_chunk",
"unknown",
&file_path.to_string_lossy(),
Confidence::High,
);
span = span.with_suggested_action(action);
if let Ok(cs) = checksum::checksum_span(file_path, span_start, span_end) {
span = span.with_checksum_before(cs.value);
}
if let Ok(file_cs) = checksum::checksum_file(file_path) {
span = span.with_file_checksum_before(file_cs.value);
}
if relationships {
use splice::relationships::{
get_exports, get_imports, RelationshipCache, Relationships,
};
let code_graph = splice::graph::CodeGraph::open(db_path)?;
let mut cache = RelationshipCache::new();
let imports =
get_imports(&code_graph, file_path, &mut cache).unwrap_or_default();
let exports =
get_exports(&code_graph, file_path, &mut cache).unwrap_or_default();
let rels = Relationships {
callers: vec![],
callees: vec![],
imports,
exports,
cycle_detected: false,
error_code: None,
};
span = span.with_relationships(rels);
}
let result = OperationResult::new("get".to_string())
.success(format!("Retrieved code chunk ({} bytes)", content.len()))
.with_result(OperationData::Query(splice::output::QueryResult {
labels: vec![],
count: 1,
symbols: vec![span],
total_count: None,
offset: None,
limit: None,
max_symbols: None,
max_bytes: None,
next_offset: None,
partial: None,
truncation_reasons: None,
}));
println!(
"{}",
serde_json::to_string_pretty(&result).expect(
"invariant: serde_json serialization never fails on serializable types"
)
);
return Ok(
splice::cli::CliSuccessPayload::message_only("OK".to_string())
.already_emitted(),
);
}
if ctx_before > 0 || ctx_after > 0 {
use splice::context;
if let Ok(ctx) = context::extract_context_asymmetric(
file_path,
expanded_start,
expanded_end,
ctx_before,
ctx_after,
) {
if !ctx.before.is_empty() {
write_stdout_line(&format!(
"Context ({} lines before):",
ctx.before.len()
))?;
for line in &ctx.before {
write_stdout_line(&format!(" {}", line))?;
}
}
}
}
write_stdout_bytes(content.as_bytes())?;
write_stdout_bytes(b"\n")?;
if ctx_before > 0 || ctx_after > 0 {
use splice::context;
if let Ok(ctx) = context::extract_context_asymmetric(
file_path,
expanded_start,
expanded_end,
ctx_before,
ctx_after,
) {
if !ctx.after.is_empty() {
write_stdout_line(&format!("Context ({} lines after):", ctx.after.len()))?;
for line in &ctx.after {
write_stdout_line(&format!(" {}", line))?;
}
}
}
}
let mut response_data = json!({
"file": file_path.to_string_lossy(),
"byte_start": start,
"byte_end": end,
"content_length": content.len(),
});
if expand && expand_level > 0 && (expanded_start != start || expanded_end != end) {
response_data["expanded_byte_start"] = json!(expanded_start);
response_data["expanded_byte_end"] = json!(expanded_end);
}
Ok(splice::cli::CliSuccessPayload::with_data(
format!("Retrieved code chunk ({} bytes)", content.len()),
response_data,
))
}
None => Ok(splice::cli::CliSuccessPayload::message_only(format!(
"No code chunk found at {}:{}-{}",
file_path.display(),
start,
end
))),
}
}