velesdb_server/handlers/query/
explain.rs1use axum::{extract::State, response::IntoResponse, Json};
4use std::sync::Arc;
5use velesdb_core::velesql::{Condition, SelectColumns};
6
7use crate::types::{ExplainCost, ExplainFeatures, ExplainRequest, ExplainResponse, ExplainStep};
8use crate::AppState;
9
10use super::velesql_helpers::{parse_and_validate, velesql_collection_not_found};
11
12#[utoipa::path(
16 post,
17 path = "/query/explain",
18 tag = "query",
19 request_body = ExplainRequest,
20 responses(
21 (status = 200, description = "Query plan", body = ExplainResponse),
22 (status = 400, description = "Query syntax error", body = crate::types::QueryErrorResponse),
23 (status = 404, description = "Collection not found", body = crate::types::VelesqlErrorResponse)
24 )
25)]
26#[allow(clippy::unused_async, deprecated)]
27pub async fn explain(
28 State(state): State<Arc<AppState>>,
29 Json(req): Json<ExplainRequest>,
30) -> impl IntoResponse {
31 let parsed = match parse_and_validate(&req.query) {
32 Ok(q) => q,
33 Err(resp) => return resp,
34 };
35
36 let select = &parsed.select;
37
38 let collection_exists = state.db.get_collection(&select.from).is_some();
39 if !collection_exists && !select.from.is_empty() {
40 return velesql_collection_not_found(&select.from);
41 }
42
43 let features = detect_explain_features(select);
44 let plan = build_explain_plan(select, &features);
45 let estimated_cost = estimate_cost(features.has_vector_search);
46
47 let query_type = if parsed.is_match_query() {
48 "MATCH"
49 } else {
50 "SELECT"
51 };
52
53 let (cache_hit, plan_reuse_count) = state
54 .db
55 .explain_query(&parsed)
56 .ok()
57 .map_or((None, None), |qp| (qp.cache_hit, qp.plan_reuse_count));
58
59 Json(ExplainResponse {
60 query: req.query,
61 query_type: query_type.to_string(),
62 collection: select.from.clone(),
63 plan,
64 estimated_cost,
65 features,
66 cache_hit,
67 plan_reuse_count,
68 })
69 .into_response()
70}
71
72fn detect_explain_features(select: &velesdb_core::velesql::SelectStatement) -> ExplainFeatures {
74 let has_vector_search = select
75 .where_clause
76 .as_ref()
77 .map(condition_has_vector_search)
78 .unwrap_or(false);
79
80 ExplainFeatures {
81 has_vector_search,
82 has_filter: select.where_clause.is_some() && !has_vector_search,
83 has_order_by: select.order_by.is_some(),
84 has_group_by: select.group_by.is_some(),
85 has_aggregation: match &select.columns {
86 SelectColumns::Aggregations(_) => true,
87 SelectColumns::Mixed { aggregations, .. } => !aggregations.is_empty(),
88 _ => false,
89 },
90 has_join: !select.joins.is_empty(),
91 has_fusion: select.fusion_clause.is_some(),
92 limit: select.limit,
93 offset: select.offset,
94 }
95}
96
97fn build_explain_plan(
99 select: &velesdb_core::velesql::SelectStatement,
100 features: &ExplainFeatures,
101) -> Vec<ExplainStep> {
102 let mut plan = Vec::new();
103 let mut step_num = 1;
104
105 plan.push(build_source_step(select, features, step_num));
106 step_num += 1;
107
108 append_filter_and_join_steps(select, features, &mut plan, &mut step_num);
109 append_aggregation_steps(features, &mut plan, &mut step_num);
110 append_pagination_step(select, &mut plan, step_num);
111
112 plan
113}
114
115fn build_source_step(
116 select: &velesdb_core::velesql::SelectStatement,
117 features: &ExplainFeatures,
118 step_num: usize,
119) -> ExplainStep {
120 if features.has_vector_search {
121 ExplainStep {
122 step: step_num,
123 operation: "VectorSearch".to_string(),
124 description: "ANN search using HNSW index with NEAR clause".to_string(),
125 estimated_rows: select.limit.map(|l| l as usize),
126 }
127 } else {
128 ExplainStep {
129 step: step_num,
130 operation: "FullScan".to_string(),
131 description: format!("Scan collection '{}'", select.from),
132 estimated_rows: None,
133 }
134 }
135}
136
137fn append_filter_and_join_steps(
138 select: &velesdb_core::velesql::SelectStatement,
139 features: &ExplainFeatures,
140 plan: &mut Vec<ExplainStep>,
141 step_num: &mut usize,
142) {
143 if features.has_filter {
144 plan.push(ExplainStep {
145 step: *step_num,
146 operation: "Filter".to_string(),
147 description: "Apply WHERE clause predicates".to_string(),
148 estimated_rows: None,
149 });
150 *step_num += 1;
151 }
152
153 for join in &select.joins {
154 plan.push(ExplainStep {
155 step: *step_num,
156 operation: format!("{:?}Join", join.join_type),
157 description: format!("Join with '{}'", join.table),
158 estimated_rows: None,
159 });
160 *step_num += 1;
161 }
162}
163
164fn append_aggregation_steps(
165 features: &ExplainFeatures,
166 plan: &mut Vec<ExplainStep>,
167 step_num: &mut usize,
168) {
169 if features.has_group_by {
170 plan.push(ExplainStep {
171 step: *step_num,
172 operation: "GroupBy".to_string(),
173 description: "Group rows by specified columns".to_string(),
174 estimated_rows: None,
175 });
176 *step_num += 1;
177 }
178
179 if features.has_aggregation {
180 plan.push(ExplainStep {
181 step: *step_num,
182 operation: "Aggregate".to_string(),
183 description: "Compute aggregate functions (COUNT, SUM, etc.)".to_string(),
184 estimated_rows: None,
185 });
186 *step_num += 1;
187 }
188
189 if features.has_order_by {
190 plan.push(ExplainStep {
191 step: *step_num,
192 operation: "Sort".to_string(),
193 description: "Sort results by ORDER BY clause".to_string(),
194 estimated_rows: None,
195 });
196 *step_num += 1;
197 }
198}
199
200fn append_pagination_step(
201 select: &velesdb_core::velesql::SelectStatement,
202 plan: &mut Vec<ExplainStep>,
203 step_num: usize,
204) {
205 if select.limit.is_some() || select.offset.is_some() {
206 plan.push(ExplainStep {
207 step: step_num,
208 operation: "Limit".to_string(),
209 description: format!(
210 "Apply LIMIT {} OFFSET {}",
211 select.limit.unwrap_or(0),
212 select.offset.unwrap_or(0)
213 ),
214 estimated_rows: select.limit.map(|l| l as usize),
215 });
216 }
217}
218
219fn estimate_cost(has_vector_search: bool) -> ExplainCost {
221 ExplainCost {
222 uses_index: has_vector_search,
223 index_name: if has_vector_search {
224 Some("HNSW".to_string())
225 } else {
226 None
227 },
228 selectivity: if has_vector_search { 0.01 } else { 1.0 },
229 complexity: if has_vector_search {
230 "O(log n)"
231 } else {
232 "O(n)"
233 }
234 .to_string(),
235 }
236}
237
238pub(super) fn condition_has_vector_search(cond: &Condition) -> bool {
240 match cond {
241 Condition::VectorSearch(_)
242 | Condition::VectorFusedSearch { .. }
243 | Condition::Similarity(_) => true,
244 Condition::And(left, right) | Condition::Or(left, right) => {
245 condition_has_vector_search(left) || condition_has_vector_search(right)
246 }
247 Condition::Group(inner) | Condition::Not(inner) => condition_has_vector_search(inner),
248 _ => false,
249 }
250}