Skip to main content

qql_core/
explain.rs

1//! Human-readable query plan inspection.
2//!
3//! Provides structured tree-formatted explanations of parsed QQL statements,
4//! detailing query execution intent, CTE subqueries, target vector spaces,
5//! routing keys, filter trees, and parameter configurations.
6
7use crate::ast::*;
8use crate::error::QqlError;
9use crate::fmt::{render_filter, render_point_selector, render_search_params};
10use crate::parser::Parser;
11use alloc::format;
12use alloc::string::String;
13use alloc::vec::Vec;
14use core::fmt::Write;
15
16/// Parse `source` and return a structured execution plan.
17pub fn explain(source: &str) -> Result<String, QqlError> {
18    let statement = Parser::parse(source)?;
19    Ok(explain_node(&statement))
20}
21
22/// Explain every statement in a semicolon-delimited script.
23/// Returns a concatenated plan, one section per statement.
24pub fn explain_all(source: &str) -> Result<String, QqlError> {
25    let statements = Parser::parse_all(source)?;
26    Ok(explain_nodes(&statements))
27}
28
29/// Explain an already parsed sequence without parsing the source again.
30pub fn explain_nodes(statements: &[Stmt]) -> String {
31    if statements.is_empty() {
32        return String::new();
33    }
34    let mut output = String::new();
35    for (i, stmt) in statements.iter().enumerate() {
36        if i > 0 {
37            output.push('\n');
38        }
39        let _ = writeln!(output, "── Statement {} ──", i + 1);
40        output.push_str(&explain_node(stmt));
41    }
42    output
43}
44
45/// Render a single AST node as a tree-structured plan.
46pub fn explain_node(statement: &Stmt) -> String {
47    let mut output = String::new();
48    match statement {
49        Stmt::Batch(batch) => {
50            let _ = writeln!(
51                output,
52                "Statement: BATCH [{} members]",
53                batch.statements.len()
54            );
55            for (i, member) in batch.statements.iter().enumerate() {
56                let _ = writeln!(output, "├── Member {}: {}", i + 1, member.stmt_kind());
57            }
58        }
59        Stmt::Query(query) => {
60            let intent = query_intent(&query.expression);
61            let _ = writeln!(output, "Statement: QUERY [{}]", intent);
62
63            let col = match &query.collection {
64                QueryCollection::Explicit(collection) => collection.as_str(),
65                QueryCollection::Inherited => "inherited",
66            };
67            let _ = writeln!(output, "├── Collection: {}", col);
68
69            if let Some(target) = query_target_vector(&query.expression) {
70                let _ = writeln!(output, "├── Target Vector: {}", target);
71            }
72
73            if let Some(shard) = &query.shard_key {
74                let _ = writeln!(output, "├── Shard Key: {shard}");
75            }
76
77            if !query.ctes.is_empty() {
78                let _ = writeln!(output, "├── CTEs ({}):", query.ctes.len());
79                for (i, cte) in query.ctes.iter().enumerate() {
80                    let is_last = i + 1 == query.ctes.len();
81                    let prefix = if is_last {
82                        "│   └──"
83                    } else {
84                        "│   ├──"
85                    };
86                    let _ = writeln!(
87                        output,
88                        "{} '{}': {}",
89                        prefix,
90                        cte.name,
91                        query_intent(&cte.query.expression)
92                    );
93                }
94            }
95
96            if let Some(prefetches) = query_prefetches(&query.expression)
97                && !prefetches.is_empty()
98            {
99                let _ = writeln!(output, "├── Prefetches ({}):", prefetches.len());
100                for (i, pf) in prefetches.iter().enumerate() {
101                    let is_last = i + 1 == prefetches.len();
102                    let prefix = if is_last {
103                        "│   └──"
104                    } else {
105                        "│   ├──"
106                    };
107                    let _ = writeln!(output, "{} [{}] {}", prefix, i + 1, pf);
108                }
109            }
110
111            if let Some(filter) = &query.filter {
112                let _ = writeln!(output, "├── Filter: {}", render_filter(filter));
113            }
114
115            if let Some(params) = &query.params {
116                let _ = writeln!(
117                    output,
118                    "├── Search Params: {}",
119                    render_search_params(params)
120                );
121            }
122
123            if let Some(score) = query.score_threshold {
124                let _ = writeln!(output, "├── Score Threshold: {}", score);
125            }
126
127            if let Some(group) = &query.group {
128                let size_str = group
129                    .size
130                    .map(|s| format!(" (size: {})", s))
131                    .unwrap_or_default();
132                let _ = writeln!(output, "├── Group By: {}{}", group.field, size_str);
133            }
134
135            if let Some(payload) = &query.output.payload {
136                match payload {
137                    PayloadSelector::Include(keys) => {
138                        let _ = writeln!(output, "├── Payload: INCLUDE ({})", keys.join(", "));
139                    }
140                    PayloadSelector::Exclude(keys) => {
141                        let _ = writeln!(output, "├── Payload: EXCLUDE ({})", keys.join(", "));
142                    }
143                    PayloadSelector::All => {
144                        output.push_str("├── Payload: ALL\n");
145                    }
146                    PayloadSelector::None => {
147                        output.push_str("├── Payload: NONE\n");
148                    }
149                }
150            }
151
152            if let Some(vectors) = &query.output.vectors {
153                match vectors {
154                    VectorSelector::Names(names) => {
155                        let _ = writeln!(output, "├── Vectors: ({})", names.join(", "));
156                    }
157                    VectorSelector::All => {
158                        output.push_str("├── Vectors: ALL\n");
159                    }
160                    VectorSelector::None => {
161                        output.push_str("├── Vectors: NONE\n");
162                    }
163                }
164            }
165
166            let limit = if let Some(param) = &query.page.limit_param {
167                param.clone()
168            } else if let Some(l) = query.page.limit {
169                l.to_string()
170            } else {
171                "default".into()
172            };
173            let offset = if let Some(param) = &query.page.offset_param {
174                param.clone()
175            } else {
176                query.page.offset.unwrap_or(0).to_string()
177            };
178            let _ = writeln!(output, "└── Pagination: limit={}, offset={}", limit, offset);
179        }
180        Stmt::Scroll(statement) => {
181            output.push_str("Statement: SCROLL\n");
182            let _ = writeln!(output, "├── Collection: {}", statement.collection);
183            if let Some(f) = &statement.filter {
184                let _ = writeln!(output, "├── Filter: {}", render_filter(f));
185            }
186            if let Some(order) = &statement.order_by {
187                let _ = writeln!(
188                    output,
189                    "├── Order By: {} {:?}",
190                    order.field, order.direction
191                );
192            }
193            if let Some(shard) = &statement.shard_key {
194                let _ = writeln!(output, "├── Shard Key: {shard}");
195            }
196            if let Some(param) = &statement.limit_param {
197                let _ = writeln!(output, "└── Limit: {}", param);
198            } else {
199                let _ = writeln!(output, "└── Limit: {}", statement.limit);
200            }
201        }
202        Stmt::Upsert(statement) => {
203            output.push_str("Statement: UPSERT\n");
204            let _ = writeln!(output, "├── Collection: {}", statement.collection);
205            let _ = writeln!(output, "├── Points: {}", statement.points.len());
206            if let Some(filter) = &statement.update_filter {
207                let _ = writeln!(
208                    output,
209                    "├── Update Filter: {}",
210                    crate::fmt::render_filter(filter)
211                );
212            }
213            if let Some(mode) = &statement.update_mode {
214                let _ = writeln!(output, "├── Update Mode: {mode:?}");
215            }
216            if let Some(shard) = &statement.shard_key {
217                let _ = writeln!(output, "├── Shard Key: {}", shard);
218            }
219            if !statement.embed.is_empty() {
220                let _ = writeln!(output, "├── Embed Directives: {}", statement.embed.len());
221            } else {
222                output.push_str("├── Status: direct payload\n");
223            }
224            if let Some(wait) = statement.wait {
225                let _ = writeln!(output, "└── Wait: {wait}");
226            }
227        }
228        Stmt::CreateCollection(statement) => {
229            let mode = match statement.mode {
230                CollectionMode::Dense { .. } => "dense",
231                CollectionMode::Hybrid { .. } => "hybrid",
232                CollectionMode::Rerank => "rerank-oriented",
233            };
234            output.push_str("Statement: CREATE COLLECTION\n");
235            let _ = writeln!(output, "├── Collection: {}", statement.collection);
236            let _ = writeln!(output, "├── Mode: {}", mode);
237            let _ = writeln!(
238                output,
239                "└── Vectors: {} dense, {} sparse",
240                statement.vectors.len(),
241                statement.sparse_vectors.len()
242            );
243        }
244        Stmt::CreateIndex(statement) => {
245            output.push_str("Statement: CREATE INDEX\n");
246            let _ = writeln!(output, "├── Collection: {}", statement.collection);
247            let _ = writeln!(output, "└── Field: {}", statement.field);
248        }
249        Stmt::CreateShardKey(statement) => {
250            output.push_str("Statement: CREATE SHARD KEY\n");
251            let _ = writeln!(output, "├── Collection: {}", statement.collection);
252            let _ = writeln!(output, "└── Shard: {}", statement.shard_key);
253        }
254        Stmt::DropShardKey(statement) => {
255            output.push_str("Statement: DROP SHARD KEY\n");
256            let _ = writeln!(output, "├── Collection: {}", statement.collection);
257            let _ = writeln!(output, "└── Shard: {}", statement.shard_key);
258        }
259        Stmt::ShowShardKeys(collection) => {
260            let _ = writeln!(output, "Statement: SHOW SHARD KEYS [{}]", collection);
261        }
262        Stmt::ShowQuotas => {
263            output.push_str("Statement: SHOW QUOTAS\n");
264        }
265        Stmt::SetQuota(statement) => {
266            output.push_str("Statement: SET QUOTA\n");
267            for (key, value) in &statement.config {
268                let _ = writeln!(output, "  {} = {}", key, render_quota_value(value));
269            }
270        }
271        Stmt::DropIndex(statement) => {
272            output.push_str("Statement: DROP INDEX\n");
273            let _ = writeln!(output, "├── Collection: {}", statement.collection);
274            let _ = writeln!(output, "└── Field: {}", statement.field);
275        }
276        Stmt::Count(statement) => {
277            output.push_str("Statement: COUNT\n");
278            let col = match &statement.collection {
279                QueryCollection::Explicit(c) => c.as_str(),
280                QueryCollection::Inherited => "inherited",
281            };
282            let _ = writeln!(output, "├── Collection: {}", col);
283            if let Some(f) = &statement.filter {
284                let _ = writeln!(output, "├── Filter: {}", render_filter(f));
285            }
286            let _ = writeln!(output, "└── Exact: {}", statement.exact.unwrap_or(false));
287        }
288        Stmt::Facet(statement) => {
289            output.push_str("Statement: FACET\n");
290            let col = match &statement.collection {
291                QueryCollection::Explicit(c) => c.as_str(),
292                QueryCollection::Inherited => "inherited",
293            };
294            let _ = writeln!(output, "├── Key: {}", statement.key);
295            let _ = writeln!(output, "├── Collection: {}", col);
296            if let Some(f) = &statement.filter {
297                let _ = writeln!(output, "├── Filter: {}", render_filter(f));
298            }
299            if let Some(shard) = &statement.shard_key {
300                let _ = writeln!(output, "├── Shard Key: {shard}");
301            }
302            if let Some(param) = &statement.limit_param {
303                let _ = writeln!(output, "├── Limit: {}", param);
304            } else if let Some(l) = statement.limit {
305                let _ = writeln!(output, "├── Limit: {}", l);
306            }
307            let _ = writeln!(output, "└── Exact: {}", statement.exact.unwrap_or(false));
308        }
309        Stmt::AlterCollection(statement) => {
310            let _ = writeln!(
311                output,
312                "Statement: ALTER COLLECTION [{}]",
313                statement.collection
314            );
315        }
316        Stmt::DropCollection(statement) => {
317            let _ = writeln!(
318                output,
319                "Statement: DROP COLLECTION [{}]",
320                statement.collection
321            );
322        }
323        Stmt::ShowCollections => {
324            output.push_str("Statement: SHOW COLLECTIONS\n");
325        }
326        Stmt::ShowCollection(collection) => {
327            let _ = writeln!(output, "Statement: SHOW COLLECTION [{}]", collection);
328        }
329        Stmt::Delete(statement) => {
330            let _ = writeln!(output, "Statement: DELETE FROM {}", statement.collection);
331            explain_mutation_tail(
332                &mut output,
333                &statement.selector,
334                statement.shard_key.as_ref(),
335                statement.wait,
336            );
337        }
338        Stmt::ClearPayload(statement) => {
339            let _ = writeln!(
340                output,
341                "Statement: CLEAR PAYLOAD ON {}",
342                statement.collection
343            );
344            explain_mutation_tail(
345                &mut output,
346                &statement.selector,
347                statement.shard_key.as_ref(),
348                statement.wait,
349            );
350        }
351        Stmt::DeletePayload(statement) => {
352            let _ = writeln!(
353                output,
354                "Statement: DELETE PAYLOAD ({:?}) ON {}",
355                statement.keys, statement.collection
356            );
357            explain_mutation_tail(
358                &mut output,
359                &statement.selector,
360                statement.shard_key.as_ref(),
361                statement.wait,
362            );
363        }
364        Stmt::DeleteVector(statement) => {
365            let _ = writeln!(
366                output,
367                "Statement: DELETE VECTOR ({:?}) ON {}",
368                statement.vector_names, statement.collection
369            );
370            explain_mutation_tail(
371                &mut output,
372                &statement.selector,
373                statement.shard_key.as_ref(),
374                statement.wait,
375            );
376        }
377        Stmt::UpdateVector(statement) => {
378            let _ = writeln!(
379                output,
380                "Statement: UPDATE VECTOR ON {}",
381                statement.collection
382            );
383            let _ = writeln!(output, "├── Points: {}", statement.points.len());
384            if let Some(shard) = &statement.shard_key {
385                let _ = writeln!(output, "├── Shard Key: {shard}");
386            }
387            if let Some(wait) = statement.wait {
388                let _ = writeln!(output, "└── Wait: {wait}");
389            }
390        }
391        Stmt::UpdatePayload(statement) => {
392            let _ = writeln!(
393                output,
394                "Statement: UPDATE PAYLOAD ON {}",
395                statement.collection
396            );
397            if let Some(key) = &statement.key {
398                let _ = writeln!(output, "├── Key: {key}");
399            }
400            if statement.overwrite {
401                output.push_str("├── Overwrite: true\n");
402            }
403            explain_mutation_tail(
404                &mut output,
405                &statement.selector,
406                statement.shard_key.as_ref(),
407                statement.wait,
408            );
409        }
410    }
411    output
412}
413
414fn query_target_vector(expression: &QueryExpr) -> Option<String> {
415    match expression {
416        QueryExpr::Nearest { using, .. }
417        | QueryExpr::Recommend { using, .. }
418        | QueryExpr::Context { using, .. }
419        | QueryExpr::Discover { using, .. } => using.as_ref().map(|vt| {
420            let kind = vt.kind.map(|k| format!(" as {:?}", k)).unwrap_or_default();
421            format!("{}{}", vt.name, kind)
422        }),
423        QueryExpr::Hybrid {
424            dense_vector,
425            sparse_vector,
426            fusion,
427            ..
428        } => {
429            let d = dense_vector.as_deref().unwrap_or("dense");
430            let s = sparse_vector.as_deref().unwrap_or("sparse");
431            let f = match fusion {
432                FusionMethod::Rrf => "RRF",
433                FusionMethod::Dbsf => "DBSF",
434            };
435            Some(format!("dense='{}', sparse='{}', fusion={}", d, s, f))
436        }
437        _ => None,
438    }
439}
440
441fn query_intent(expression: &QueryExpr) -> &'static str {
442    match expression {
443        QueryExpr::Points { .. } => "retrieve points by ID",
444        QueryExpr::Nearest { mmr: Some(_), .. } => "maximal marginal relevance (MMR) search",
445        QueryExpr::Nearest { input, .. } => match input {
446            QueryInput::Text { .. } => "nearest neighbors from text",
447            QueryInput::Image { .. } => "nearest neighbors from an image",
448            QueryInput::Object { .. } => "nearest neighbors from an inference object",
449            QueryInput::Vector(_) => "nearest neighbors from a vector",
450            QueryInput::Point(_) => "nearest neighbors from a point",
451            QueryInput::Param(..) | QueryInput::PositionalParam(..) => {
452                "nearest neighbors from query parameter"
453            }
454        },
455        QueryExpr::Recommend { .. } => "recommend from positive and negative examples",
456        QueryExpr::Context { .. } => "context search",
457        QueryExpr::Discover { .. } => "discovery search",
458        QueryExpr::OrderBy { .. } => "payload order query",
459        QueryExpr::SampleRandom => "random sample",
460        QueryExpr::Fusion { .. } => "fuse prefetched result sets",
461        QueryExpr::Formula { .. } => "formula-based scoring",
462        QueryExpr::RelevanceFeedback { .. } => "relevance feedback",
463        QueryExpr::Hybrid { .. } => "hybrid shorthand (dense + sparse)",
464        QueryExpr::Rerank { .. } => "late-interaction prefetched rerank",
465        QueryExpr::CrossRerank { .. } => "cross-encoder pair rerank of prefetched candidates",
466    }
467}
468
469fn query_prefetches(expression: &QueryExpr) -> Option<Vec<String>> {
470    match expression {
471        QueryExpr::Nearest { prefetch, .. }
472        | QueryExpr::Recommend { prefetch, .. }
473        | QueryExpr::Context { prefetch, .. }
474        | QueryExpr::Discover { prefetch, .. }
475        | QueryExpr::Fusion { prefetch, .. }
476        | QueryExpr::Formula { prefetch, .. }
477        | QueryExpr::RelevanceFeedback { prefetch, .. }
478        | QueryExpr::Rerank { prefetch, .. }
479        | QueryExpr::CrossRerank { prefetch, .. } => Some(
480            prefetch
481                .iter()
482                .map(|p| match &p.source {
483                    PrefetchSource::Cte(name) => format!("CTE '{}'", name),
484                    PrefetchSource::Query(q) => {
485                        format!("inline query: {}", query_intent(&q.expression))
486                    }
487                })
488                .collect(),
489        ),
490        _ => None,
491    }
492}
493
494fn explain_mutation_tail(
495    output: &mut String,
496    selector: &PointSelector,
497    shard_key: Option<&ShardKey>,
498    wait: Option<bool>,
499) {
500    let _ = writeln!(output, "├── Selector: {}", render_point_selector(selector));
501    if let Some(shard) = shard_key {
502        let _ = writeln!(output, "├── Shard Key: {shard}");
503    }
504    if let Some(wait) = wait {
505        let _ = writeln!(output, "└── Wait: {wait}");
506    }
507}
508
509fn render_quota_value(value: &Value) -> String {
510    match value {
511        Value::Str(s) => format!("'{}'", s),
512        Value::Int(n) => n.to_string(),
513        Value::UInt(n) => n.to_string(),
514        Value::Float(f) => f.to_string(),
515        Value::Bool(b) => b.to_string(),
516        Value::Null => "null".into(),
517        Value::Dict(_) => "<object>".into(),
518        Value::List(_) | Value::F32Array(_) => "<list>".into(),
519        Value::Param(name, _) => format!(":{}", name),
520        Value::PositionalParam(..) => "?".into(),
521    }
522}
523
524#[cfg(test)]
525mod tests {
526    use super::*;
527
528    #[test]
529    fn test_explain_tree_structure() {
530        let q = "QUERY TEXT 'chest pain' FROM medical USING dense WHERE department = 'cardio' SHARD 'east' LIMIT 5;";
531        let plan = explain(q).unwrap();
532        assert!(plan.contains("Statement: QUERY [nearest neighbors from text]"));
533        assert!(plan.contains("├── Collection: medical"));
534        assert!(plan.contains("├── Target Vector: dense"));
535        assert!(plan.contains("├── Shard Key: 'east'"));
536        assert!(plan.contains("├── Filter: department = 'cardio'"));
537        assert!(plan.contains("└── Pagination: limit=5, offset=0"));
538    }
539
540    #[test]
541    fn explain_dml_includes_selector_and_shard() {
542        let plan =
543            explain("DELETE FROM docs WHERE status = 'archived' SHARD 'east' WAIT true;").unwrap();
544        assert!(plan.contains("Statement: DELETE FROM docs"));
545        assert!(plan.contains("Selector:"));
546        assert!(plan.contains("status = 'archived'"));
547        assert!(plan.contains("Shard Key: 'east'"));
548        assert!(plan.contains("Wait: true"));
549    }
550
551    #[test]
552    fn explain_upsert_includes_shard_and_wait() {
553        let plan =
554            explain("UPSERT INTO docs VALUES {id: 1, text: 'a'} SHARD 101 WAIT true;").unwrap();
555        assert!(plan.contains("Statement: UPSERT"));
556        assert!(plan.contains("Shard Key: 101"));
557        assert!(plan.contains("Wait: true"));
558        let plain = explain("UPSERT INTO docs VALUES {id: 1, text: 'a'};").unwrap();
559        assert!(!plain.contains("Wait:"));
560    }
561}