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> =
132            node.children.iter().map(format_node_json).collect();
133        parts.push(format!("\"children\": [{}]", children.join(", ")));
134    }
135
136    format!("{{{}}}", parts.join(", "))
137}
138
139/// Executor for EXPLAIN statements
140pub struct ExplainExecutor;
141
142impl ExplainExecutor {
143    /// Execute an EXPLAIN statement
144    pub fn execute(stmt: &ExplainStmt, database: &Database) -> Result<ExplainResult, ExecutorError> {
145        let plan = match stmt.statement.as_ref() {
146            Statement::Select(select_stmt) => Self::explain_select(select_stmt, database)?,
147            Statement::Insert(_) => {
148                PlanNode::new("Insert").with_detail("Inserts rows into target table".to_string())
149            }
150            Statement::Update(_) => PlanNode::new("Update")
151                .with_detail("Updates rows matching WHERE clause".to_string()),
152            Statement::Delete(_) => PlanNode::new("Delete")
153                .with_detail("Deletes rows matching WHERE clause".to_string()),
154            _ => {
155                return Err(ExecutorError::Other(
156                    "EXPLAIN only supports SELECT, INSERT, UPDATE, DELETE statements".to_string(),
157                ));
158            }
159        };
160
161        Ok(ExplainResult { plan, format: stmt.format.clone() })
162    }
163
164    /// Generate execution plan for a SELECT statement
165    fn explain_select(stmt: &SelectStmt, database: &Database) -> Result<PlanNode, ExecutorError> {
166        let mut root = PlanNode::new("Select");
167
168        // Analyze FROM clause
169        if let Some(ref from_clause) = stmt.from {
170            let scan_node =
171                Self::explain_from_clause(from_clause, &stmt.where_clause, &stmt.order_by, database)?;
172            root.add_child(scan_node);
173        }
174
175        // Add WHERE clause info
176        if stmt.where_clause.is_some() {
177            root.details.push("Filter: <where clause>".to_string());
178        }
179
180        // Add GROUP BY info
181        if stmt.group_by.is_some() {
182            root.details.push("Group: <group by clause>".to_string());
183        }
184
185        // Add ORDER BY info
186        if stmt.order_by.is_some() {
187            root.details.push("Sort: <order by clause>".to_string());
188        }
189
190        // Add LIMIT info
191        if let Some(limit) = stmt.limit {
192            root.details.push(format!("Limit: {}", limit));
193        }
194
195        Ok(root)
196    }
197
198    /// Generate plan node for FROM clause
199    fn explain_from_clause(
200        from: &vibesql_ast::FromClause,
201        where_clause: &Option<vibesql_ast::Expression>,
202        order_by: &Option<Vec<vibesql_ast::OrderByItem>>,
203        database: &Database,
204    ) -> Result<PlanNode, ExecutorError> {
205        match from {
206            vibesql_ast::FromClause::Table { name, alias, .. } => {
207                Self::explain_table_scan(name, alias.as_deref(), where_clause, order_by, database)
208            }
209            vibesql_ast::FromClause::Join { left, right, join_type, condition, natural } => {
210                let join_name = match join_type {
211                    vibesql_ast::JoinType::Inner => "Inner Join",
212                    vibesql_ast::JoinType::LeftOuter => "Left Outer Join",
213                    vibesql_ast::JoinType::RightOuter => "Right Outer Join",
214                    vibesql_ast::JoinType::FullOuter => "Full Outer Join",
215                    vibesql_ast::JoinType::Cross => "Cross Join",
216                    vibesql_ast::JoinType::Semi => "Semi Join",
217                    vibesql_ast::JoinType::Anti => "Anti Join",
218                };
219
220                let mut join_node = PlanNode::new(join_name);
221
222                if *natural {
223                    join_node.details.push("NATURAL join".to_string());
224                }
225
226                if condition.is_some() {
227                    join_node.details.push("Join condition: <on clause>".to_string());
228                }
229
230                // Add left child
231                let left_child = Self::explain_from_clause(left, where_clause, order_by, database)?;
232                join_node.add_child(left_child);
233
234                // Add right child (no WHERE pushdown for right side in simple case)
235                let right_child = Self::explain_from_clause(right, &None, &None, database)?;
236                join_node.add_child(right_child);
237
238                Ok(join_node)
239            }
240            vibesql_ast::FromClause::Subquery { query, alias, .. } => {
241                let mut subquery_node = PlanNode::new("Subquery");
242                subquery_node.object = Some(format!("AS {}", alias));
243
244                let child = Self::explain_select(query, database)?;
245                subquery_node.add_child(child);
246
247                Ok(subquery_node)
248            }
249        }
250    }
251
252    /// Generate plan node for table scan (sequential or index)
253    fn explain_table_scan(
254        table_name: &str,
255        alias: Option<&str>,
256        where_clause: &Option<vibesql_ast::Expression>,
257        order_by: &Option<Vec<vibesql_ast::OrderByItem>>,
258        database: &Database,
259    ) -> Result<PlanNode, ExecutorError> {
260        // Check if we can use an index
261        let index_info = cost_based_index_selection(
262            table_name,
263            where_clause.as_ref(),
264            order_by.as_ref().map(|v| v.as_slice()),
265            database,
266        );
267
268        let mut node = if let Some((index_name, sorted_cols)) = index_info {
269            let mut idx_node = PlanNode::new("Index Scan").with_object(table_name);
270            idx_node.details.push(format!("Using index: {}", index_name));
271
272            if let Some(cols) = sorted_cols {
273                let col_strs: Vec<String> = cols
274                    .iter()
275                    .map(|(col, dir)| {
276                        format!(
277                            "{} {}",
278                            col,
279                            match dir {
280                                vibesql_ast::OrderDirection::Asc => "ASC",
281                                vibesql_ast::OrderDirection::Desc => "DESC",
282                            }
283                        )
284                    })
285                    .collect();
286                idx_node.details.push(format!("Sorted by: {}", col_strs.join(", ")));
287            }
288
289            idx_node
290        } else {
291            PlanNode::new("Seq Scan").with_object(table_name)
292        };
293
294        // Add alias if present
295        if let Some(a) = alias {
296            node.details.push(format!("Alias: {}", a));
297        }
298
299        // Add row estimate if table exists
300        if let Some(table) = database.get_table(table_name) {
301            let row_count = table.row_count();
302            node = node.with_estimated_rows(row_count as f64);
303        }
304
305        Ok(node)
306    }
307}