use crate::ast::*;
use crate::error::QqlError;
use crate::fmt::{render_filter, render_point_selector, render_search_params};
use crate::parser::Parser;
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt::Write;
pub fn explain(source: &str) -> Result<String, QqlError> {
let statement = Parser::parse(source)?;
Ok(explain_node(&statement))
}
pub fn explain_all(source: &str) -> Result<String, QqlError> {
let statements = Parser::parse_all(source)?;
Ok(explain_nodes(&statements))
}
pub fn explain_nodes(statements: &[Stmt]) -> String {
if statements.is_empty() {
return String::new();
}
let mut output = String::new();
for (i, stmt) in statements.iter().enumerate() {
if i > 0 {
output.push('\n');
}
let _ = writeln!(output, "── Statement {} ──", i + 1);
output.push_str(&explain_node(stmt));
}
output
}
pub fn explain_node(statement: &Stmt) -> String {
let mut output = String::new();
match statement {
Stmt::Batch(batch) => {
let _ = writeln!(
output,
"Statement: BATCH [{} members]",
batch.statements.len()
);
for (i, member) in batch.statements.iter().enumerate() {
let _ = writeln!(output, "├── Member {}: {}", i + 1, member.stmt_kind());
}
}
Stmt::Query(query) => {
let intent = query_intent(&query.expression);
let _ = writeln!(output, "Statement: QUERY [{}]", intent);
let col = match &query.collection {
QueryCollection::Explicit(collection) => collection.as_str(),
QueryCollection::Inherited => "inherited",
};
let _ = writeln!(output, "├── Collection: {}", col);
if let Some(target) = query_target_vector(&query.expression) {
let _ = writeln!(output, "├── Target Vector: {}", target);
}
if let Some(shard) = &query.shard_key {
let _ = writeln!(output, "├── Shard Key: {shard}");
}
if !query.ctes.is_empty() {
let _ = writeln!(output, "├── CTEs ({}):", query.ctes.len());
for (i, cte) in query.ctes.iter().enumerate() {
let is_last = i + 1 == query.ctes.len();
let prefix = if is_last {
"│ └──"
} else {
"│ ├──"
};
let _ = writeln!(
output,
"{} '{}': {}",
prefix,
cte.name,
query_intent(&cte.query.expression)
);
}
}
if let Some(prefetches) = query_prefetches(&query.expression)
&& !prefetches.is_empty()
{
let _ = writeln!(output, "├── Prefetches ({}):", prefetches.len());
for (i, pf) in prefetches.iter().enumerate() {
let is_last = i + 1 == prefetches.len();
let prefix = if is_last {
"│ └──"
} else {
"│ ├──"
};
let _ = writeln!(output, "{} [{}] {}", prefix, i + 1, pf);
}
}
if let Some(filter) = &query.filter {
let _ = writeln!(output, "├── Filter: {}", render_filter(filter));
}
if let Some(params) = &query.params {
let _ = writeln!(
output,
"├── Search Params: {}",
render_search_params(params)
);
}
if let Some(score) = query.score_threshold {
let _ = writeln!(output, "├── Score Threshold: {}", score);
}
if let Some(group) = &query.group {
let size_str = group
.size
.map(|s| format!(" (size: {})", s))
.unwrap_or_default();
let _ = writeln!(output, "├── Group By: {}{}", group.field, size_str);
}
if let Some(payload) = &query.output.payload {
match payload {
PayloadSelector::Include(keys) => {
let _ = writeln!(output, "├── Payload: INCLUDE ({})", keys.join(", "));
}
PayloadSelector::Exclude(keys) => {
let _ = writeln!(output, "├── Payload: EXCLUDE ({})", keys.join(", "));
}
PayloadSelector::All => {
output.push_str("├── Payload: ALL\n");
}
PayloadSelector::None => {
output.push_str("├── Payload: NONE\n");
}
}
}
if let Some(vectors) = &query.output.vectors {
match vectors {
VectorSelector::Names(names) => {
let _ = writeln!(output, "├── Vectors: ({})", names.join(", "));
}
VectorSelector::All => {
output.push_str("├── Vectors: ALL\n");
}
VectorSelector::None => {
output.push_str("├── Vectors: NONE\n");
}
}
}
let limit = if let Some(param) = &query.page.limit_param {
param.clone()
} else if let Some(l) = query.page.limit {
l.to_string()
} else {
"default".into()
};
let offset = if let Some(param) = &query.page.offset_param {
param.clone()
} else {
query.page.offset.unwrap_or(0).to_string()
};
let _ = writeln!(output, "└── Pagination: limit={}, offset={}", limit, offset);
}
Stmt::Scroll(statement) => {
output.push_str("Statement: SCROLL\n");
let _ = writeln!(output, "├── Collection: {}", statement.collection);
if let Some(f) = &statement.filter {
let _ = writeln!(output, "├── Filter: {}", render_filter(f));
}
if let Some(order) = &statement.order_by {
let _ = writeln!(
output,
"├── Order By: {} {:?}",
order.field, order.direction
);
}
if let Some(shard) = &statement.shard_key {
let _ = writeln!(output, "├── Shard Key: {shard}");
}
if let Some(param) = &statement.limit_param {
let _ = writeln!(output, "└── Limit: {}", param);
} else {
let _ = writeln!(output, "└── Limit: {}", statement.limit);
}
}
Stmt::Upsert(statement) => {
output.push_str("Statement: UPSERT\n");
let _ = writeln!(output, "├── Collection: {}", statement.collection);
let _ = writeln!(output, "├── Points: {}", statement.points.len());
if let Some(filter) = &statement.update_filter {
let _ = writeln!(
output,
"├── Update Filter: {}",
crate::fmt::render_filter(filter)
);
}
if let Some(mode) = &statement.update_mode {
let _ = writeln!(output, "├── Update Mode: {mode:?}");
}
if let Some(shard) = &statement.shard_key {
let _ = writeln!(output, "├── Shard Key: {}", shard);
}
if !statement.embed.is_empty() {
let _ = writeln!(output, "├── Embed Directives: {}", statement.embed.len());
} else {
output.push_str("├── Status: direct payload\n");
}
if let Some(wait) = statement.wait {
let _ = writeln!(output, "└── Wait: {wait}");
}
}
Stmt::CreateCollection(statement) => {
let mode = match statement.mode {
CollectionMode::Dense { .. } => "dense",
CollectionMode::Hybrid { .. } => "hybrid",
CollectionMode::Rerank => "rerank-oriented",
};
output.push_str("Statement: CREATE COLLECTION\n");
let _ = writeln!(output, "├── Collection: {}", statement.collection);
let _ = writeln!(output, "├── Mode: {}", mode);
let _ = writeln!(
output,
"└── Vectors: {} dense, {} sparse",
statement.vectors.len(),
statement.sparse_vectors.len()
);
}
Stmt::CreateIndex(statement) => {
output.push_str("Statement: CREATE INDEX\n");
let _ = writeln!(output, "├── Collection: {}", statement.collection);
let _ = writeln!(output, "└── Field: {}", statement.field);
}
Stmt::CreateShardKey(statement) => {
output.push_str("Statement: CREATE SHARD KEY\n");
let _ = writeln!(output, "├── Collection: {}", statement.collection);
let _ = writeln!(output, "└── Shard: {}", statement.shard_key);
}
Stmt::DropShardKey(statement) => {
output.push_str("Statement: DROP SHARD KEY\n");
let _ = writeln!(output, "├── Collection: {}", statement.collection);
let _ = writeln!(output, "└── Shard: {}", statement.shard_key);
}
Stmt::ShowShardKeys(collection) => {
let _ = writeln!(output, "Statement: SHOW SHARD KEYS [{}]", collection);
}
Stmt::ShowQuotas => {
output.push_str("Statement: SHOW QUOTAS\n");
}
Stmt::SetQuota(statement) => {
output.push_str("Statement: SET QUOTA\n");
for (key, value) in &statement.config {
let _ = writeln!(output, " {} = {}", key, render_quota_value(value));
}
}
Stmt::DropIndex(statement) => {
output.push_str("Statement: DROP INDEX\n");
let _ = writeln!(output, "├── Collection: {}", statement.collection);
let _ = writeln!(output, "└── Field: {}", statement.field);
}
Stmt::Count(statement) => {
output.push_str("Statement: COUNT\n");
let col = match &statement.collection {
QueryCollection::Explicit(c) => c.as_str(),
QueryCollection::Inherited => "inherited",
};
let _ = writeln!(output, "├── Collection: {}", col);
if let Some(f) = &statement.filter {
let _ = writeln!(output, "├── Filter: {}", render_filter(f));
}
let _ = writeln!(output, "└── Exact: {}", statement.exact.unwrap_or(false));
}
Stmt::Facet(statement) => {
output.push_str("Statement: FACET\n");
let col = match &statement.collection {
QueryCollection::Explicit(c) => c.as_str(),
QueryCollection::Inherited => "inherited",
};
let _ = writeln!(output, "├── Key: {}", statement.key);
let _ = writeln!(output, "├── Collection: {}", col);
if let Some(f) = &statement.filter {
let _ = writeln!(output, "├── Filter: {}", render_filter(f));
}
if let Some(shard) = &statement.shard_key {
let _ = writeln!(output, "├── Shard Key: {shard}");
}
if let Some(param) = &statement.limit_param {
let _ = writeln!(output, "├── Limit: {}", param);
} else if let Some(l) = statement.limit {
let _ = writeln!(output, "├── Limit: {}", l);
}
let _ = writeln!(output, "└── Exact: {}", statement.exact.unwrap_or(false));
}
Stmt::AlterCollection(statement) => {
let _ = writeln!(
output,
"Statement: ALTER COLLECTION [{}]",
statement.collection
);
}
Stmt::DropCollection(statement) => {
let _ = writeln!(
output,
"Statement: DROP COLLECTION [{}]",
statement.collection
);
}
Stmt::ShowCollections => {
output.push_str("Statement: SHOW COLLECTIONS\n");
}
Stmt::ShowCollection(collection) => {
let _ = writeln!(output, "Statement: SHOW COLLECTION [{}]", collection);
}
Stmt::Delete(statement) => {
let _ = writeln!(output, "Statement: DELETE FROM {}", statement.collection);
explain_mutation_tail(
&mut output,
&statement.selector,
statement.shard_key.as_ref(),
statement.wait,
);
}
Stmt::ClearPayload(statement) => {
let _ = writeln!(
output,
"Statement: CLEAR PAYLOAD ON {}",
statement.collection
);
explain_mutation_tail(
&mut output,
&statement.selector,
statement.shard_key.as_ref(),
statement.wait,
);
}
Stmt::DeletePayload(statement) => {
let _ = writeln!(
output,
"Statement: DELETE PAYLOAD ({:?}) ON {}",
statement.keys, statement.collection
);
explain_mutation_tail(
&mut output,
&statement.selector,
statement.shard_key.as_ref(),
statement.wait,
);
}
Stmt::DeleteVector(statement) => {
let _ = writeln!(
output,
"Statement: DELETE VECTOR ({:?}) ON {}",
statement.vector_names, statement.collection
);
explain_mutation_tail(
&mut output,
&statement.selector,
statement.shard_key.as_ref(),
statement.wait,
);
}
Stmt::UpdateVector(statement) => {
let _ = writeln!(
output,
"Statement: UPDATE VECTOR ON {}",
statement.collection
);
let _ = writeln!(output, "├── Points: {}", statement.points.len());
if let Some(shard) = &statement.shard_key {
let _ = writeln!(output, "├── Shard Key: {shard}");
}
if let Some(wait) = statement.wait {
let _ = writeln!(output, "└── Wait: {wait}");
}
}
Stmt::UpdatePayload(statement) => {
let _ = writeln!(
output,
"Statement: UPDATE PAYLOAD ON {}",
statement.collection
);
if let Some(key) = &statement.key {
let _ = writeln!(output, "├── Key: {key}");
}
if statement.overwrite {
output.push_str("├── Overwrite: true\n");
}
explain_mutation_tail(
&mut output,
&statement.selector,
statement.shard_key.as_ref(),
statement.wait,
);
}
}
output
}
fn query_target_vector(expression: &QueryExpr) -> Option<String> {
match expression {
QueryExpr::Nearest { using, .. }
| QueryExpr::Recommend { using, .. }
| QueryExpr::Context { using, .. }
| QueryExpr::Discover { using, .. } => using.as_ref().map(|vt| {
let kind = vt.kind.map(|k| format!(" as {:?}", k)).unwrap_or_default();
format!("{}{}", vt.name, kind)
}),
QueryExpr::Hybrid {
dense_vector,
sparse_vector,
fusion,
..
} => {
let d = dense_vector.as_deref().unwrap_or("dense");
let s = sparse_vector.as_deref().unwrap_or("sparse");
let f = match fusion {
FusionMethod::Rrf => "RRF",
FusionMethod::Dbsf => "DBSF",
};
Some(format!("dense='{}', sparse='{}', fusion={}", d, s, f))
}
_ => None,
}
}
fn query_intent(expression: &QueryExpr) -> &'static str {
match expression {
QueryExpr::Points { .. } => "retrieve points by ID",
QueryExpr::Nearest { mmr: Some(_), .. } => "maximal marginal relevance (MMR) search",
QueryExpr::Nearest { input, .. } => match input {
QueryInput::Text { .. } => "nearest neighbors from text",
QueryInput::Image { .. } => "nearest neighbors from an image",
QueryInput::Object { .. } => "nearest neighbors from an inference object",
QueryInput::Vector(_) => "nearest neighbors from a vector",
QueryInput::Point(_) => "nearest neighbors from a point",
QueryInput::Param(..) | QueryInput::PositionalParam(..) => {
"nearest neighbors from query parameter"
}
},
QueryExpr::Recommend { .. } => "recommend from positive and negative examples",
QueryExpr::Context { .. } => "context search",
QueryExpr::Discover { .. } => "discovery search",
QueryExpr::OrderBy { .. } => "payload order query",
QueryExpr::SampleRandom => "random sample",
QueryExpr::Fusion { .. } => "fuse prefetched result sets",
QueryExpr::Formula { .. } => "formula-based scoring",
QueryExpr::RelevanceFeedback { .. } => "relevance feedback",
QueryExpr::Hybrid { .. } => "hybrid shorthand (dense + sparse)",
QueryExpr::Rerank { .. } => "late-interaction prefetched rerank",
QueryExpr::CrossRerank { .. } => "cross-encoder pair rerank of prefetched candidates",
}
}
fn query_prefetches(expression: &QueryExpr) -> Option<Vec<String>> {
match expression {
QueryExpr::Nearest { prefetch, .. }
| QueryExpr::Recommend { prefetch, .. }
| QueryExpr::Context { prefetch, .. }
| QueryExpr::Discover { prefetch, .. }
| QueryExpr::Fusion { prefetch, .. }
| QueryExpr::Formula { prefetch, .. }
| QueryExpr::RelevanceFeedback { prefetch, .. }
| QueryExpr::Rerank { prefetch, .. }
| QueryExpr::CrossRerank { prefetch, .. } => Some(
prefetch
.iter()
.map(|p| match &p.source {
PrefetchSource::Cte(name) => format!("CTE '{}'", name),
PrefetchSource::Query(q) => {
format!("inline query: {}", query_intent(&q.expression))
}
})
.collect(),
),
_ => None,
}
}
fn explain_mutation_tail(
output: &mut String,
selector: &PointSelector,
shard_key: Option<&ShardKey>,
wait: Option<bool>,
) {
let _ = writeln!(output, "├── Selector: {}", render_point_selector(selector));
if let Some(shard) = shard_key {
let _ = writeln!(output, "├── Shard Key: {shard}");
}
if let Some(wait) = wait {
let _ = writeln!(output, "└── Wait: {wait}");
}
}
fn render_quota_value(value: &Value) -> String {
match value {
Value::Str(s) => format!("'{}'", s),
Value::Int(n) => n.to_string(),
Value::UInt(n) => n.to_string(),
Value::Float(f) => f.to_string(),
Value::Bool(b) => b.to_string(),
Value::Null => "null".into(),
Value::Dict(_) => "<object>".into(),
Value::List(_) | Value::F32Array(_) => "<list>".into(),
Value::Param(name, _) => format!(":{}", name),
Value::PositionalParam(..) => "?".into(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_explain_tree_structure() {
let q = "QUERY TEXT 'chest pain' FROM medical USING dense WHERE department = 'cardio' SHARD 'east' LIMIT 5;";
let plan = explain(q).unwrap();
assert!(plan.contains("Statement: QUERY [nearest neighbors from text]"));
assert!(plan.contains("├── Collection: medical"));
assert!(plan.contains("├── Target Vector: dense"));
assert!(plan.contains("├── Shard Key: 'east'"));
assert!(plan.contains("├── Filter: department = 'cardio'"));
assert!(plan.contains("└── Pagination: limit=5, offset=0"));
}
#[test]
fn explain_dml_includes_selector_and_shard() {
let plan =
explain("DELETE FROM docs WHERE status = 'archived' SHARD 'east' WAIT true;").unwrap();
assert!(plan.contains("Statement: DELETE FROM docs"));
assert!(plan.contains("Selector:"));
assert!(plan.contains("status = 'archived'"));
assert!(plan.contains("Shard Key: 'east'"));
assert!(plan.contains("Wait: true"));
}
#[test]
fn explain_upsert_includes_shard_and_wait() {
let plan =
explain("UPSERT INTO docs VALUES {id: 1, text: 'a'} SHARD 101 WAIT true;").unwrap();
assert!(plan.contains("Statement: UPSERT"));
assert!(plan.contains("Shard Key: 101"));
assert!(plan.contains("Wait: true"));
let plain = explain("UPSERT INTO docs VALUES {id: 1, text: 'a'};").unwrap();
assert!(!plain.contains("Wait:"));
}
}