1use std::fmt::Write;
12use vibesql_ast::{ExplainFormat, ExplainStmt, SelectStmt, Statement};
13use vibesql_storage::Database;
14
15use crate::errors::ExecutorError;
16use crate::optimizer::index_planner::IndexPlanner;
17use crate::select::scan::index_scan::cost_based_index_selection;
18
19#[derive(Debug, Clone)]
21pub struct PlanNode {
22 pub operation: String,
24 pub object: Option<String>,
26 pub details: Vec<String>,
28 pub estimated_rows: Option<f64>,
30 pub children: Vec<PlanNode>,
32}
33
34impl PlanNode {
35 fn new(operation: &str) -> Self {
36 PlanNode {
37 operation: operation.to_string(),
38 object: None,
39 details: Vec::new(),
40 estimated_rows: None,
41 children: Vec::new(),
42 }
43 }
44
45 fn with_object(mut self, object: &str) -> Self {
46 self.object = Some(object.to_string());
47 self
48 }
49
50 fn with_detail(mut self, detail: String) -> Self {
51 self.details.push(detail);
52 self
53 }
54
55 fn with_estimated_rows(mut self, rows: f64) -> Self {
56 self.estimated_rows = Some(rows);
57 self
58 }
59
60 fn add_child(&mut self, child: PlanNode) {
61 self.children.push(child);
62 }
63}
64
65#[derive(Debug)]
67pub struct ExplainResult {
68 pub plan: PlanNode,
70 pub format: ExplainFormat,
72}
73
74impl ExplainResult {
75 pub fn to_text(&self) -> String {
77 let mut output = String::new();
78 format_node_text(&self.plan, 0, &mut output);
79 output
80 }
81
82 pub fn to_json(&self) -> String {
84 format_node_json(&self.plan)
85 }
86}
87
88fn format_node_text(node: &PlanNode, depth: usize, output: &mut String) {
89 let indent = " ".repeat(depth);
90 let arrow = if depth > 0 { "-> " } else { "" };
91
92 let mut line = format!("{}{}{}", indent, arrow, node.operation);
94
95 if let Some(ref obj) = node.object {
96 write!(line, " on {}", obj).unwrap();
97 }
98
99 if let Some(rows) = node.estimated_rows {
100 write!(line, " (rows={:.0})", rows).unwrap();
101 }
102
103 writeln!(output, "{}", line).unwrap();
104
105 for detail in &node.details {
107 writeln!(output, "{} {}", indent, detail).unwrap();
108 }
109
110 for child in &node.children {
112 format_node_text(child, depth + 1, output);
113 }
114}
115
116fn format_node_json(node: &PlanNode) -> String {
117 let mut parts = vec![format!("\"operation\": \"{}\"", node.operation)];
118
119 if let Some(ref obj) = node.object {
120 parts.push(format!("\"object\": \"{}\"", obj));
121 }
122
123 if !node.details.is_empty() {
124 let details: Vec<String> = node.details.iter().map(|d| format!("\"{}\"", d)).collect();
125 parts.push(format!("\"details\": [{}]", details.join(", ")));
126 }
127
128 if let Some(rows) = node.estimated_rows {
129 parts.push(format!("\"estimated_rows\": {:.0}", rows));
130 }
131
132 if !node.children.is_empty() {
133 let children: Vec<String> = node.children.iter().map(format_node_json).collect();
134 parts.push(format!("\"children\": [{}]", children.join(", ")));
135 }
136
137 format!("{{{}}}", parts.join(", "))
138}
139
140pub struct ExplainExecutor;
142
143impl ExplainExecutor {
144 pub fn execute(
146 stmt: &ExplainStmt,
147 database: &Database,
148 ) -> Result<ExplainResult, ExecutorError> {
149 let plan = match stmt.statement.as_ref() {
150 Statement::Select(select_stmt) => Self::explain_select(select_stmt, database)?,
151 Statement::Insert(_) => {
152 PlanNode::new("Insert").with_detail("Inserts rows into target table".to_string())
153 }
154 Statement::Update(_) => PlanNode::new("Update")
155 .with_detail("Updates rows matching WHERE clause".to_string()),
156 Statement::Delete(_) => PlanNode::new("Delete")
157 .with_detail("Deletes rows matching WHERE clause".to_string()),
158 _ => {
159 return Err(ExecutorError::Other(
160 "EXPLAIN only supports SELECT, INSERT, UPDATE, DELETE statements".to_string(),
161 ));
162 }
163 };
164
165 Ok(ExplainResult { plan, format: stmt.format.clone() })
166 }
167
168 fn explain_select(stmt: &SelectStmt, database: &Database) -> Result<PlanNode, ExecutorError> {
170 let mut root = PlanNode::new("Select");
171
172 if let Some(ref from_clause) = stmt.from {
174 let scan_node = Self::explain_from_clause(
175 from_clause,
176 &stmt.where_clause,
177 &stmt.order_by,
178 database,
179 )?;
180 root.add_child(scan_node);
181 }
182
183 if stmt.where_clause.is_some() {
185 root.details.push("Filter: <where clause>".to_string());
186 }
187
188 if stmt.group_by.is_some() {
190 root.details.push("Group: <group by clause>".to_string());
191 }
192
193 if stmt.order_by.is_some() {
195 root.details.push("Sort: <order by clause>".to_string());
196 }
197
198 if let Some(limit) = stmt.limit {
200 root.details.push(format!("Limit: {}", limit));
201 }
202
203 Ok(root)
204 }
205
206 fn explain_from_clause(
208 from: &vibesql_ast::FromClause,
209 where_clause: &Option<vibesql_ast::Expression>,
210 order_by: &Option<Vec<vibesql_ast::OrderByItem>>,
211 database: &Database,
212 ) -> Result<PlanNode, ExecutorError> {
213 match from {
214 vibesql_ast::FromClause::Table { name, alias, .. } => {
215 Self::explain_table_scan(name, alias.as_deref(), where_clause, order_by, database)
216 }
217 vibesql_ast::FromClause::Join { left, right, join_type, condition, natural } => {
218 let join_name = match join_type {
219 vibesql_ast::JoinType::Inner => "Inner Join",
220 vibesql_ast::JoinType::LeftOuter => "Left Outer Join",
221 vibesql_ast::JoinType::RightOuter => "Right Outer Join",
222 vibesql_ast::JoinType::FullOuter => "Full Outer Join",
223 vibesql_ast::JoinType::Cross => "Cross Join",
224 vibesql_ast::JoinType::Semi => "Semi Join",
225 vibesql_ast::JoinType::Anti => "Anti Join",
226 };
227
228 let mut join_node = PlanNode::new(join_name);
229
230 if *natural {
231 join_node.details.push("NATURAL join".to_string());
232 }
233
234 if condition.is_some() {
235 join_node.details.push("Join condition: <on clause>".to_string());
236 }
237
238 let left_child = Self::explain_from_clause(left, where_clause, order_by, database)?;
240 join_node.add_child(left_child);
241
242 let right_child = Self::explain_from_clause(right, &None, &None, database)?;
244 join_node.add_child(right_child);
245
246 Ok(join_node)
247 }
248 vibesql_ast::FromClause::Subquery { query, alias, .. } => {
249 let mut subquery_node = PlanNode::new("Subquery");
250 subquery_node.object = Some(format!("AS {}", alias));
251
252 let child = Self::explain_select(query, database)?;
253 subquery_node.add_child(child);
254
255 Ok(subquery_node)
256 }
257 }
258 }
259
260 fn explain_table_scan(
262 table_name: &str,
263 alias: Option<&str>,
264 where_clause: &Option<vibesql_ast::Expression>,
265 order_by: &Option<Vec<vibesql_ast::OrderByItem>>,
266 database: &Database,
267 ) -> Result<PlanNode, ExecutorError> {
268 let index_info = cost_based_index_selection(
270 table_name,
271 where_clause.as_ref(),
272 order_by.as_ref().map(|v| v.as_slice()),
273 database,
274 );
275
276 let skip_scan_plan = if index_info.is_none() {
278 if let Some(where_expr) = where_clause {
279 let planner = IndexPlanner::new(database);
280 planner.plan_skip_scan(table_name, where_expr)
281 } else {
282 None
283 }
284 } else {
285 None
286 };
287
288 let mut node = if let Some(skip_plan) = skip_scan_plan {
289 let skip_info = skip_plan.skip_scan_info.as_ref().unwrap();
291
292 let mut skip_node = PlanNode::new("Skip Scan").with_object(table_name);
293 skip_node.details.push(format!("Using index: {}", skip_plan.index_name));
294 skip_node.details.push(format!(
295 "Skip columns: {} (cardinality: {})",
296 skip_info.prefix_columns.join(", "),
297 skip_info.prefix_cardinality
298 ));
299 skip_node.details.push(format!("Filter column: {}", skip_info.filter_column));
300 skip_node.details.push(format!("Estimated cost: {:.2}", skip_info.estimated_cost));
301
302 skip_node
303 } else if let Some((index_name, sorted_cols)) = index_info {
304 let mut idx_node = PlanNode::new("Index Scan").with_object(table_name);
305 idx_node.details.push(format!("Using index: {}", index_name));
306
307 if let Some(cols) = sorted_cols {
308 let col_strs: Vec<String> = cols
309 .iter()
310 .map(|(col, dir)| {
311 format!(
312 "{} {}",
313 col,
314 match dir {
315 vibesql_ast::OrderDirection::Asc => "ASC",
316 vibesql_ast::OrderDirection::Desc => "DESC",
317 }
318 )
319 })
320 .collect();
321 idx_node.details.push(format!("Sorted by: {}", col_strs.join(", ")));
322 }
323
324 idx_node
325 } else {
326 PlanNode::new("Seq Scan").with_object(table_name)
327 };
328
329 if let Some(a) = alias {
331 node.details.push(format!("Alias: {}", a));
332 }
333
334 if let Some(table) = database.get_table(table_name) {
336 let row_count = table.row_count();
337 node = node.with_estimated_rows(row_count as f64);
338 }
339
340 Ok(node)
341 }
342}