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//! - Skip-scan optimization (non-prefix index usage)
7//! - Join types and order
8//! - Filter pushdown information
9//! - Estimated row counts (when statistics are available)
10
11use 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/// Represents a single node in the query execution plan
20#[derive(Debug, Clone)]
21pub struct PlanNode {
22    /// Type of operation (e.g., "Seq Scan", "Index Scan", "Hash Join")
23    pub operation: String,
24    /// Target object (e.g., table name, index name)
25    pub object: Option<String>,
26    /// Additional details about this operation
27    pub details: Vec<String>,
28    /// Estimated rows (if statistics available)
29    pub estimated_rows: Option<f64>,
30    /// Child nodes in the plan tree
31    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/// Result of EXPLAIN operation
66#[derive(Debug)]
67pub struct ExplainResult {
68    /// Root node of the execution plan
69    pub plan: PlanNode,
70    /// Output format
71    pub format: ExplainFormat,
72}
73
74impl ExplainResult {
75    /// Format the plan as text output
76    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    /// Format the plan as JSON output
83    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    // Format the main operation line
93    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    // Format details
106    for detail in &node.details {
107        writeln!(output, "{}      {}", indent, detail).unwrap();
108    }
109
110    // Format children
111    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
140/// Executor for EXPLAIN statements
141pub struct ExplainExecutor;
142
143impl ExplainExecutor {
144    /// Execute an EXPLAIN statement
145    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    /// Generate execution plan for a SELECT statement
169    fn explain_select(stmt: &SelectStmt, database: &Database) -> Result<PlanNode, ExecutorError> {
170        let mut root = PlanNode::new("Select");
171
172        // Analyze FROM clause
173        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        // Add WHERE clause info
184        if stmt.where_clause.is_some() {
185            root.details.push("Filter: <where clause>".to_string());
186        }
187
188        // Add GROUP BY info
189        if stmt.group_by.is_some() {
190            root.details.push("Group: <group by clause>".to_string());
191        }
192
193        // Add ORDER BY info
194        if stmt.order_by.is_some() {
195            root.details.push("Sort: <order by clause>".to_string());
196        }
197
198        // Add LIMIT info
199        if let Some(limit) = stmt.limit {
200            root.details.push(format!("Limit: {}", limit));
201        }
202
203        Ok(root)
204    }
205
206    /// Generate plan node for FROM clause
207    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                // Add left child
239                let left_child = Self::explain_from_clause(left, where_clause, order_by, database)?;
240                join_node.add_child(left_child);
241
242                // Add right child (no WHERE pushdown for right side in simple case)
243                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    /// Generate plan node for table scan (sequential or index)
261    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        // First check for regular index scan
269        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        // If no regular index scan, check for skip-scan optimization
277        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            // Skip-scan detected - display skip-scan specific information
290            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        // Add alias if present
330        if let Some(a) = alias {
331            node.details.push(format!("Alias: {}", a));
332        }
333
334        // Add row estimate if table exists
335        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}