Skip to main content

oxigdal_query/
explain.rs

1//! Query plan explanation and visualization.
2
3use crate::optimizer::OptimizedQuery;
4use crate::optimizer::cost_model::Cost;
5use crate::parser::ast::*;
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9/// Query explain output.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ExplainPlan {
12    /// Query plan nodes.
13    pub nodes: Vec<ExplainNode>,
14    /// Total estimated cost.
15    pub total_cost: Cost,
16    /// Execution statistics (if available).
17    pub statistics: Option<ExecutionStatistics>,
18}
19
20impl ExplainPlan {
21    /// Create explain plan from optimized query.
22    pub fn from_optimized(query: &OptimizedQuery) -> Self {
23        let nodes = Self::build_nodes(&query.statement);
24        Self {
25            nodes,
26            total_cost: query.optimized_cost,
27            statistics: None,
28        }
29    }
30
31    /// Build explain nodes from statement.
32    fn build_nodes(stmt: &Statement) -> Vec<ExplainNode> {
33        match stmt {
34            Statement::Select(select) => Self::build_select_nodes(select),
35        }
36    }
37
38    /// Build nodes for SELECT statement.
39    fn build_select_nodes(select: &SelectStatement) -> Vec<ExplainNode> {
40        let mut nodes = Vec::new();
41        let mut node_id = 0;
42
43        // FROM clause
44        if let Some(ref table_ref) = select.from {
45            Self::build_table_nodes(table_ref, &mut nodes, &mut node_id, 0);
46        }
47
48        // WHERE clause
49        if select.selection.is_some() {
50            nodes.push(ExplainNode {
51                id: node_id,
52                node_type: NodeType::Filter,
53                description: "Filter".to_string(),
54                details: format!("Predicate: {:?}", select.selection),
55                cost: Cost::zero(),
56                rows: None,
57                depth: 0,
58            });
59            node_id += 1;
60        }
61
62        // GROUP BY / Aggregation
63        if !select.group_by.is_empty() {
64            nodes.push(ExplainNode {
65                id: node_id,
66                node_type: NodeType::Aggregate,
67                description: "Aggregate".to_string(),
68                details: format!("Group by: {:?}", select.group_by),
69                cost: Cost::zero(),
70                rows: None,
71                depth: 0,
72            });
73            node_id += 1;
74        }
75
76        // ORDER BY
77        if !select.order_by.is_empty() {
78            nodes.push(ExplainNode {
79                id: node_id,
80                node_type: NodeType::Sort,
81                description: "Sort".to_string(),
82                details: format!("Order by: {:?}", select.order_by),
83                cost: Cost::zero(),
84                rows: None,
85                depth: 0,
86            });
87            node_id += 1;
88        }
89
90        // LIMIT
91        if select.limit.is_some() {
92            nodes.push(ExplainNode {
93                id: node_id,
94                node_type: NodeType::Limit,
95                description: "Limit".to_string(),
96                details: format!("Limit: {:?}", select.limit),
97                cost: Cost::zero(),
98                rows: select.limit,
99                depth: 0,
100            });
101        }
102
103        nodes
104    }
105
106    /// Build nodes for table reference.
107    fn build_table_nodes(
108        table_ref: &TableReference,
109        nodes: &mut Vec<ExplainNode>,
110        node_id: &mut usize,
111        depth: usize,
112    ) {
113        match table_ref {
114            TableReference::Table { name, .. } => {
115                nodes.push(ExplainNode {
116                    id: *node_id,
117                    node_type: NodeType::TableScan,
118                    description: "Table Scan".to_string(),
119                    details: format!("Table: {}", name),
120                    cost: Cost::zero(),
121                    rows: None,
122                    depth,
123                });
124                *node_id += 1;
125            }
126            TableReference::Join {
127                left,
128                right,
129                join_type,
130                ..
131            } => {
132                Self::build_table_nodes(left, nodes, node_id, depth + 1);
133                Self::build_table_nodes(right, nodes, node_id, depth + 1);
134
135                nodes.push(ExplainNode {
136                    id: *node_id,
137                    node_type: NodeType::Join,
138                    description: format!("{:?} Join", join_type),
139                    details: String::new(),
140                    cost: Cost::zero(),
141                    rows: None,
142                    depth,
143                });
144                *node_id += 1;
145            }
146            TableReference::Subquery { query, alias } => {
147                let subquery_nodes = Self::build_select_nodes(query);
148                nodes.extend(subquery_nodes);
149
150                nodes.push(ExplainNode {
151                    id: *node_id,
152                    node_type: NodeType::Subquery,
153                    description: "Subquery".to_string(),
154                    details: format!("Alias: {}", alias),
155                    cost: Cost::zero(),
156                    rows: None,
157                    depth,
158                });
159                *node_id += 1;
160            }
161        }
162    }
163
164    /// Format as text.
165    pub fn format_text(&self) -> String {
166        let mut output = String::new();
167        output.push_str("Query Execution Plan:\n");
168        output.push_str(&format!("Total Cost: {:.2}\n", self.total_cost.total()));
169        output.push('\n');
170
171        for node in &self.nodes {
172            let indent = "  ".repeat(node.depth);
173            output.push_str(&format!(
174                "{}[{}] {}: {}\n",
175                indent, node.id, node.description, node.details
176            ));
177            if let Some(rows) = node.rows {
178                output.push_str(&format!("{}    Rows: {}\n", indent, rows));
179            }
180            output.push_str(&format!("{}    Cost: {:.2}\n", indent, node.cost.total()));
181        }
182
183        if let Some(ref stats) = self.statistics {
184            output.push_str("\nExecution Statistics:\n");
185            output.push_str(&format!("  Execution Time: {:?}\n", stats.execution_time));
186            output.push_str(&format!("  Rows Processed: {}\n", stats.rows_processed));
187            output.push_str(&format!("  Rows Returned: {}\n", stats.rows_returned));
188        }
189
190        output
191    }
192
193    /// Format as JSON.
194    pub fn format_json(&self) -> Result<String, serde_json::Error> {
195        serde_json::to_string_pretty(self)
196    }
197}
198
199/// Query plan node.
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct ExplainNode {
202    /// Node ID.
203    pub id: usize,
204    /// Node type.
205    pub node_type: NodeType,
206    /// Description.
207    pub description: String,
208    /// Additional details.
209    pub details: String,
210    /// Estimated cost.
211    pub cost: Cost,
212    /// Estimated rows.
213    pub rows: Option<usize>,
214    /// Tree depth.
215    pub depth: usize,
216}
217
218/// Node type in query plan.
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
220pub enum NodeType {
221    /// Table scan.
222    TableScan,
223    /// Index scan.
224    IndexScan,
225    /// Filter operation.
226    Filter,
227    /// Join operation.
228    Join,
229    /// Aggregate operation.
230    Aggregate,
231    /// Sort operation.
232    Sort,
233    /// Limit operation.
234    Limit,
235    /// Subquery.
236    Subquery,
237}
238
239/// Execution statistics.
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct ExecutionStatistics {
242    /// Total execution time.
243    pub execution_time: std::time::Duration,
244    /// Number of rows processed.
245    pub rows_processed: usize,
246    /// Number of rows returned.
247    pub rows_returned: usize,
248    /// Peak memory usage in bytes.
249    pub peak_memory: Option<usize>,
250}
251
252impl fmt::Display for ExplainPlan {
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254        write!(f, "{}", self.format_text())
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use crate::parser::sql::parse_sql;
262
263    #[test]
264    fn test_explain_plan() {
265        let sql = "SELECT id, name FROM users WHERE age > 18 ORDER BY name LIMIT 10";
266        let stmt = parse_sql(sql).ok();
267
268        if let Some(stmt) = stmt {
269            let nodes = ExplainPlan::build_nodes(&stmt);
270            assert!(!nodes.is_empty());
271        }
272    }
273
274    #[test]
275    fn test_explain_format_text() {
276        let plan = ExplainPlan {
277            nodes: vec![ExplainNode {
278                id: 0,
279                node_type: NodeType::TableScan,
280                description: "Table Scan".to_string(),
281                details: "Table: users".to_string(),
282                cost: Cost::zero(),
283                rows: Some(1000),
284                depth: 0,
285            }],
286            total_cost: Cost::new(100.0, 1000.0, 100.0, 0.0),
287            statistics: None,
288        };
289
290        let text = plan.format_text();
291        assert!(text.contains("Query Execution Plan"));
292        assert!(text.contains("Table Scan"));
293    }
294}