vibesql_executor/
explain.rs

1//! EXPLAIN statement execution
2//!
3//! This module provides the ExplainExecutor for analyzing query execution plans.
4//! It shows information about:
5//! - Table scans vs index scans
6//! - Join types and order
7//! - Filter pushdown information
8//! - Estimated row counts (when statistics are available)
9
10use std::fmt::Write;
11use vibesql_ast::{ExplainFormat, ExplainStmt, SelectStmt, Statement};
12use vibesql_storage::Database;
13
14use crate::errors::ExecutorError;
15use crate::select::scan::index_scan::cost_based_index_selection;
16
17/// Represents a single node in the query execution plan
18#[derive(Debug, Clone)]
19pub struct PlanNode {
20    /// Type of operation (e.g., "Seq Scan", "Index Scan", "Hash Join")
21    pub operation: String,
22    /// Target object (e.g., table name, index name)
23    pub object: Option<String>,
24    /// Additional details about this operation
25    pub details: Vec<String>,
26    /// Estimated rows (if statistics available)
27    pub estimated_rows: Option<f64>,
28    /// Child nodes in the plan tree
29    pub children: Vec<PlanNode>,
30}
31
32impl PlanNode {
33    fn new(operation: &str) -> Self {
34        PlanNode {
35            operation: operation.to_string(),
36            object: None,
37            details: Vec::new(),
38            estimated_rows: None,
39            children: Vec::new(),
40        }
41    }
42
43    fn with_object(mut self, object: &str) -> Self {
44        self.object = Some(object.to_string());
45        self
46    }
47
48    fn with_detail(mut self, detail: String) -> Self {
49        self.details.push(detail);
50        self
51    }
52
53    fn with_estimated_rows(mut self, rows: f64) -> Self {
54        self.estimated_rows = Some(rows);
55        self
56    }
57
58    fn add_child(&mut self, child: PlanNode) {
59        self.children.push(child);
60    }
61}
62
63/// Result of EXPLAIN operation
64#[derive(Debug)]
65pub struct ExplainResult {
66    /// Root node of the execution plan
67    pub plan: PlanNode,
68    /// Output format
69    pub format: ExplainFormat,
70}
71
72impl ExplainResult {
73    /// Format the plan as text output
74    pub fn to_text(&self) -> String {
75        let mut output = String::new();
76        format_node_text(&self.plan, 0, &mut output);
77        output
78    }
79
80    /// Format the plan as JSON output
81    pub fn to_json(&self) -> String {
82        format_node_json(&self.plan)
83    }
84}
85
86fn format_node_text(node: &PlanNode, depth: usize, output: &mut String) {
87    let indent = "  ".repeat(depth);
88    let arrow = if depth > 0 { "-> " } else { "" };
89
90    // Format the main operation line
91    let mut line = format!("{}{}{}", indent, arrow, node.operation);
92
93    if let Some(ref obj) = node.object {
94        write!(line, " on {}", obj).unwrap();
95    }
96
97    if let Some(rows) = node.estimated_rows {
98        write!(line, "  (rows={:.0})", rows).unwrap();
99    }
100
101    writeln!(output, "{}", line).unwrap();
102
103    // Format details
104    for detail in &node.details {
105        writeln!(output, "{}      {}", indent, detail).unwrap();
106    }
107
108    // Format children
109    for child in &node.children {
110        format_node_text(child, depth + 1, output);
111    }
112}
113
114fn format_node_json(node: &PlanNode) -> String {
115    let mut parts = vec![format!("\"operation\": \"{}\"", node.operation)];
116
117    if let Some(ref obj) = node.object {
118        parts.push(format!("\"object\": \"{}\"", obj));
119    }
120
121    if !node.details.is_empty() {
122        let details: Vec<String> = node.details.iter().map(|d| format!("\"{}\"", d)).collect();
123        parts.push(format!("\"details\": [{}]", details.join(", ")));
124    }
125
126    if let Some(rows) = node.estimated_rows {
127        parts.push(format!("\"estimated_rows\": {:.0}", rows));
128    }
129
130    if !node.children.is_empty() {
131        let children: Vec<String> = node.children.iter().map(format_node_json).collect();
132        parts.push(format!("\"children\": [{}]", children.join(", ")));
133    }
134
135    format!("{{{}}}", parts.join(", "))
136}
137
138/// Executor for EXPLAIN statements
139pub struct ExplainExecutor;
140
141impl ExplainExecutor {
142    /// Execute an EXPLAIN statement
143    pub fn execute(
144        stmt: &ExplainStmt,
145        database: &Database,
146    ) -> Result<ExplainResult, ExecutorError> {
147        let plan = match stmt.statement.as_ref() {
148            Statement::Select(select_stmt) => Self::explain_select(select_stmt, database)?,
149            Statement::Insert(_) => {
150                PlanNode::new("Insert").with_detail("Inserts rows into target table".to_string())
151            }
152            Statement::Update(_) => PlanNode::new("Update")
153                .with_detail("Updates rows matching WHERE clause".to_string()),
154            Statement::Delete(_) => PlanNode::new("Delete")
155                .with_detail("Deletes rows matching WHERE clause".to_string()),
156            _ => {
157                return Err(ExecutorError::Other(
158                    "EXPLAIN only supports SELECT, INSERT, UPDATE, DELETE statements".to_string(),
159                ));
160            }
161        };
162
163        Ok(ExplainResult { plan, format: stmt.format.clone() })
164    }
165
166    /// Generate execution plan for a SELECT statement
167    fn explain_select(stmt: &SelectStmt, database: &Database) -> Result<PlanNode, ExecutorError> {
168        let mut root = PlanNode::new("Select");
169
170        // Analyze FROM clause
171        if let Some(ref from_clause) = stmt.from {
172            let scan_node = Self::explain_from_clause(
173                from_clause,
174                &stmt.where_clause,
175                &stmt.order_by,
176                database,
177            )?;
178            root.add_child(scan_node);
179        }
180
181        // Add WHERE clause info
182        if stmt.where_clause.is_some() {
183            root.details.push("Filter: <where clause>".to_string());
184        }
185
186        // Add GROUP BY info
187        if stmt.group_by.is_some() {
188            root.details.push("Group: <group by clause>".to_string());
189        }
190
191        // Add ORDER BY info
192        if stmt.order_by.is_some() {
193            root.details.push("Sort: <order by clause>".to_string());
194        }
195
196        // Add LIMIT info
197        if let Some(limit) = stmt.limit {
198            root.details.push(format!("Limit: {}", limit));
199        }
200
201        Ok(root)
202    }
203
204    /// Generate plan node for FROM clause
205    fn explain_from_clause(
206        from: &vibesql_ast::FromClause,
207        where_clause: &Option<vibesql_ast::Expression>,
208        order_by: &Option<Vec<vibesql_ast::OrderByItem>>,
209        database: &Database,
210    ) -> Result<PlanNode, ExecutorError> {
211        match from {
212            vibesql_ast::FromClause::Table { name, alias, .. } => {
213                Self::explain_table_scan(name, alias.as_deref(), where_clause, order_by, database)
214            }
215            vibesql_ast::FromClause::Join { left, right, join_type, condition, natural } => {
216                let join_name = match join_type {
217                    vibesql_ast::JoinType::Inner => "Inner Join",
218                    vibesql_ast::JoinType::LeftOuter => "Left Outer Join",
219                    vibesql_ast::JoinType::RightOuter => "Right Outer Join",
220                    vibesql_ast::JoinType::FullOuter => "Full Outer Join",
221                    vibesql_ast::JoinType::Cross => "Cross Join",
222                    vibesql_ast::JoinType::Semi => "Semi Join",
223                    vibesql_ast::JoinType::Anti => "Anti Join",
224                };
225
226                let mut join_node = PlanNode::new(join_name);
227
228                if *natural {
229                    join_node.details.push("NATURAL join".to_string());
230                }
231
232                if condition.is_some() {
233                    join_node.details.push("Join condition: <on clause>".to_string());
234                }
235
236                // Add left child
237                let left_child = Self::explain_from_clause(left, where_clause, order_by, database)?;
238                join_node.add_child(left_child);
239
240                // Add right child (no WHERE pushdown for right side in simple case)
241                let right_child = Self::explain_from_clause(right, &None, &None, database)?;
242                join_node.add_child(right_child);
243
244                Ok(join_node)
245            }
246            vibesql_ast::FromClause::Subquery { query, alias, .. } => {
247                let mut subquery_node = PlanNode::new("Subquery");
248                subquery_node.object = Some(format!("AS {}", alias));
249
250                let child = Self::explain_select(query, database)?;
251                subquery_node.add_child(child);
252
253                Ok(subquery_node)
254            }
255        }
256    }
257
258    /// Generate plan node for table scan (sequential or index)
259    fn explain_table_scan(
260        table_name: &str,
261        alias: Option<&str>,
262        where_clause: &Option<vibesql_ast::Expression>,
263        order_by: &Option<Vec<vibesql_ast::OrderByItem>>,
264        database: &Database,
265    ) -> Result<PlanNode, ExecutorError> {
266        // Check if we can use an index
267        let index_info = cost_based_index_selection(
268            table_name,
269            where_clause.as_ref(),
270            order_by.as_ref().map(|v| v.as_slice()),
271            database,
272        );
273
274        let mut node = if let Some((index_name, sorted_cols)) = index_info {
275            let mut idx_node = PlanNode::new("Index Scan").with_object(table_name);
276            idx_node.details.push(format!("Using index: {}", index_name));
277
278            if let Some(cols) = sorted_cols {
279                let col_strs: Vec<String> = cols
280                    .iter()
281                    .map(|(col, dir)| {
282                        format!(
283                            "{} {}",
284                            col,
285                            match dir {
286                                vibesql_ast::OrderDirection::Asc => "ASC",
287                                vibesql_ast::OrderDirection::Desc => "DESC",
288                            }
289                        )
290                    })
291                    .collect();
292                idx_node.details.push(format!("Sorted by: {}", col_strs.join(", ")));
293            }
294
295            idx_node
296        } else {
297            PlanNode::new("Seq Scan").with_object(table_name)
298        };
299
300        // Add alias if present
301        if let Some(a) = alias {
302            node.details.push(format!("Alias: {}", a));
303        }
304
305        // Add row estimate if table exists
306        if let Some(table) = database.get_table(table_name) {
307            let row_count = table.row_count();
308            node = node.with_estimated_rows(row_count as f64);
309        }
310
311        Ok(node)
312    }
313}