1use 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
16pub fn explain(source: &str) -> Result<String, QqlError> {
18 let statement = Parser::parse(source)?;
19 Ok(explain_node(&statement))
20}
21
22pub fn explain_all(source: &str) -> Result<String, QqlError> {
25 let statements = Parser::parse_all(source)?;
26 Ok(explain_nodes(&statements))
27}
28
29pub 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
45pub 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}