Skip to main content

qql_core/
explain.rs

1use crate::ast::{CollectionMode, QueryCollection, QueryExpr, QueryInput, Stmt};
2use crate::error::QqlError;
3use crate::parser::Parser;
4use alloc::format;
5use alloc::string::String;
6
7pub fn explain(source: &str) -> Result<String, QqlError> {
8    let statement = Parser::parse(source)?;
9    Ok(explain_node(&statement))
10}
11
12/// Explain every statement in a semicolon-delimited script.
13/// Returns a concatenated plan, one section per statement.
14pub fn explain_all(source: &str) -> Result<String, QqlError> {
15    let statements = Parser::parse_all(source)?;
16    Ok(explain_nodes(&statements))
17}
18
19/// Explain an already parsed sequence without parsing the source again.
20pub fn explain_nodes(statements: &[Stmt]) -> String {
21    if statements.is_empty() {
22        return String::new();
23    }
24    let mut output = String::new();
25    for (i, stmt) in statements.iter().enumerate() {
26        if i > 0 {
27            output.push('\n');
28        }
29        output.push_str(&format!("--- Statement {} ---\n", i + 1));
30        output.push_str(&explain_node(stmt));
31    }
32    output
33}
34
35pub fn explain_node(statement: &Stmt) -> String {
36    let mut output = String::new();
37    match statement {
38        Stmt::Query(query) => {
39            output.push_str("Statement: QUERY\n");
40            output.push_str(&format!("Intent: {}\n", query_intent(&query.expression)));
41            match &query.collection {
42                QueryCollection::Explicit(collection) => {
43                    output.push_str(&format!("Collection: {}\n", collection));
44                }
45                QueryCollection::Inherited => output.push_str("Collection: inherited\n"),
46            }
47            if !query.ctes.is_empty() {
48                output.push_str(&format!("CTEs: {}\n", query.ctes.len()));
49            }
50            if query.filter.is_some() {
51                output.push_str("Filter: present\n");
52            }
53            if let Some(limit) = query.page.limit {
54                output.push_str(&format!("Limit: {}\n", limit));
55            }
56        }
57        Stmt::Scroll(statement) => output.push_str(&format!(
58            "Statement: SCROLL\nCollection: {}\nLimit: {}\n",
59            statement.collection, statement.limit
60        )),
61        Stmt::Upsert(statement) => output.push_str(&format!(
62            "Statement: UPSERT\nCollection: {}\nPoints: {}\n",
63            statement.collection,
64            statement.points.len()
65        )),
66        Stmt::CreateCollection(statement) => {
67            let mode = match statement.mode {
68                CollectionMode::Dense { .. } => "dense",
69                CollectionMode::Hybrid { .. } => "hybrid",
70                CollectionMode::Rerank => "rerank-oriented",
71            };
72            output.push_str(&format!(
73                "Statement: CREATE COLLECTION\nCollection: {}\nDeclared mode: {}\n",
74                statement.collection, mode
75            ));
76        }
77        Stmt::CreateIndex(statement) => output.push_str(&format!(
78            "Statement: CREATE INDEX\nCollection: {}\nField: {}\n",
79            statement.collection, statement.field
80        )),
81        Stmt::CreateShardKey(statement) => output.push_str(&format!(
82            "Statement: CREATE SHARD KEY\nCollection: {}\nShard: {}\n",
83            statement.collection, statement.shard_key
84        )),
85        Stmt::DropShardKey(statement) => output.push_str(&format!(
86            "Statement: DROP SHARD KEY\nCollection: {}\nShard: {}\n",
87            statement.collection, statement.shard_key
88        )),
89        Stmt::ShowShardKeys(collection) => output.push_str(&format!(
90            "Statement: SHOW SHARD KEYS\nCollection: {}\n",
91            collection
92        )),
93        Stmt::DropIndex(statement) => output.push_str(&format!(
94            "Statement: DROP INDEX\nCollection: {}\nField: {}\n",
95            statement.collection, statement.field
96        )),
97        Stmt::Count(statement) => {
98            output.push_str("Statement: COUNT\n");
99            match &statement.collection {
100                QueryCollection::Explicit(collection) => {
101                    output.push_str(&format!("Collection: {}\n", collection));
102                }
103                QueryCollection::Inherited => output.push_str("Collection: inherited\n"),
104            }
105            if statement.filter.is_some() {
106                output.push_str("Filter: present\n");
107            }
108        }
109        Stmt::AlterCollection(statement) => output.push_str(&format!(
110            "Statement: ALTER COLLECTION\nCollection: {}\n",
111            statement.collection
112        )),
113        Stmt::DropCollection(statement) => output.push_str(&format!(
114            "Statement: DROP COLLECTION\nCollection: {}\n",
115            statement.collection
116        )),
117        Stmt::ShowCollections => output.push_str("Statement: SHOW COLLECTIONS\n"),
118        Stmt::ShowCollection(collection) => {
119            output.push_str(&format!(
120                "Statement: SHOW COLLECTION\nCollection: {}\n",
121                collection
122            ));
123        }
124        Stmt::Delete(statement) => output.push_str(&format!(
125            "Statement: DELETE\nCollection: {}\nSelector: typed point selector\n",
126            statement.collection
127        )),
128        Stmt::ClearPayload(statement) => output.push_str(&format!(
129            "Statement: CLEAR PAYLOAD\nCollection: {}\nSelector: typed point selector\n",
130            statement.collection
131        )),
132        Stmt::DeletePayload(statement) => output.push_str(&format!(
133            "Statement: DELETE PAYLOAD\nCollection: {}\nKeys: {:?}\nSelector: typed point selector\n",
134            statement.collection, statement.keys
135        )),
136        Stmt::DeleteVector(statement) => output.push_str(&format!(
137            "Statement: DELETE VECTOR\nCollection: {}\nVectors: {:?}\nSelector: typed point selector\n",
138            statement.collection, statement.vector_names
139        )),
140        Stmt::UpdateVector(statement) => output.push_str(&format!(
141            "Statement: UPDATE VECTOR\nCollection: {}\n",
142            statement.collection
143        )),
144        Stmt::UpdatePayload(statement) => output.push_str(&format!(
145            "Statement: UPDATE PAYLOAD\nCollection: {}\n",
146            statement.collection
147        )),
148    }
149    output
150}
151
152fn query_intent(expression: &QueryExpr) -> &'static str {
153    match expression {
154        QueryExpr::Points { .. } => "retrieve points by ID",
155        QueryExpr::Nearest { mmr: Some(_), .. } => "maximal marginal relevance (MMR) search",
156        QueryExpr::Nearest { input, .. } => match input {
157            QueryInput::Text { .. } => "nearest neighbors from text",
158            QueryInput::Image { .. } => "nearest neighbors from an image",
159            QueryInput::Vector(_) => "nearest neighbors from a vector",
160            QueryInput::Point(_) => "nearest neighbors from a point",
161        },
162        QueryExpr::Recommend { .. } => "recommend from positive and negative examples",
163        QueryExpr::Context { .. } => "context search",
164        QueryExpr::Discover { .. } => "discovery search",
165        QueryExpr::OrderBy { .. } => "payload order query",
166        QueryExpr::SampleRandom => "random sample",
167        QueryExpr::Fusion { .. } => "fuse prefetched result sets",
168        QueryExpr::Formula { .. } => "formula-based scoring",
169        QueryExpr::RelevanceFeedback { .. } => "relevance feedback",
170        QueryExpr::Hybrid { .. } => "hybrid shorthand",
171        QueryExpr::Rerank { .. } => "late-interaction prefetched rerank",
172        QueryExpr::CrossRerank { .. } => "cross-encoder pair rerank of prefetched candidates",
173    }
174}