1use std::fmt::Write;
16
17use std::collections::HashSet;
18
19use vibesql_ast::pretty_print::ToSql;
20use vibesql_ast::{ExplainFormat, ExplainStmt, Expression, SelectItem, SelectStmt, Statement};
21use vibesql_storage::Database;
22
23use crate::{
24 errors::ExecutorError, optimizer::index_planner::IndexPlanner,
25 select::scan::index_scan::cost_based_index_selection,
26};
27
28#[derive(Debug, Clone, PartialEq)]
30pub enum ScanType {
31 Scan,
33 Search,
35 CoveringIndex,
37 IntegerPrimaryKey,
39}
40
41#[derive(Debug, Clone)]
43pub struct IndexPredicate {
44 pub column: String,
46 pub predicate_type: String,
48}
49
50#[derive(Debug, Clone)]
52pub struct PlanNode {
53 pub operation: String,
55 pub object: Option<String>,
57 pub details: Vec<String>,
59 pub estimated_rows: Option<f64>,
61 pub children: Vec<PlanNode>,
63 pub scan_type: Option<ScanType>,
65 pub index_name: Option<String>,
67 pub index_predicates: Vec<IndexPredicate>,
69 pub needs_temp_btree_for_order_by: bool,
71 pub set_operation_type: Option<String>,
73 pub is_compound_query: bool,
75}
76
77impl PlanNode {
78 fn new(operation: &str) -> Self {
79 PlanNode {
80 operation: operation.to_string(),
81 object: None,
82 details: Vec::new(),
83 estimated_rows: None,
84 children: Vec::new(),
85 scan_type: None,
86 index_name: None,
87 index_predicates: Vec::new(),
88 needs_temp_btree_for_order_by: false,
89 set_operation_type: None,
90 is_compound_query: false,
91 }
92 }
93
94 fn with_object(mut self, object: &str) -> Self {
95 self.object = Some(object.to_string());
96 self
97 }
98
99 fn with_detail(mut self, detail: String) -> Self {
100 self.details.push(detail);
101 self
102 }
103
104 fn with_estimated_rows(mut self, rows: f64) -> Self {
105 self.estimated_rows = Some(rows);
106 self
107 }
108
109 fn with_scan_type(mut self, scan_type: ScanType) -> Self {
110 self.scan_type = Some(scan_type);
111 self
112 }
113
114 fn with_index_name(mut self, index_name: &str) -> Self {
115 self.index_name = Some(index_name.to_string());
116 self
117 }
118
119 fn with_index_predicate(mut self, column: &str, predicate_type: &str) -> Self {
120 self.index_predicates.push(IndexPredicate {
121 column: column.to_string(),
122 predicate_type: predicate_type.to_string(),
123 });
124 self
125 }
126
127 fn add_child(&mut self, child: PlanNode) {
128 self.children.push(child);
129 }
130}
131
132#[derive(Debug)]
134pub struct ExplainResult {
135 pub plan: PlanNode,
137 pub format: ExplainFormat,
139}
140
141impl ExplainResult {
142 pub fn to_text(&self) -> String {
144 let mut output = String::new();
145 format_node_text(&self.plan, 0, &mut output);
146 output
147 }
148
149 pub fn to_json(&self) -> String {
151 format_node_json(&self.plan)
152 }
153
154 pub fn to_sqlite_eqp(&self) -> String {
176 let mut output = String::new();
177
178 writeln!(output, "QUERY PLAN").unwrap();
180
181 if self.plan.is_compound_query {
183 format_compound_query_eqp(&self.plan, "", true, &mut output);
184 } else {
185 let entries = collect_eqp_entries(&self.plan);
187
188 for (i, entry) in entries.iter().enumerate() {
189 let is_last = i == entries.len() - 1;
190 let prefix = if is_last { "`--" } else { "|--" };
191 writeln!(output, "{}{}", prefix, entry).unwrap();
192 }
193 }
194
195 output
196 }
197
198 pub fn to_sqlite_vm(&self) -> SqliteVmOutput {
212 let mut instructions = Vec::new();
213 let mut addr = 0;
214
215 let scan_nodes = collect_scan_nodes(&self.plan);
217
218 let init_addr = addr;
221 instructions.push(VmInstruction {
222 addr,
223 opcode: "Init".to_string(),
224 p1: 0,
225 p2: 0, p3: 0,
227 p4: String::new(),
228 p5: 0,
229 comment: "Start at ?".to_string(),
230 });
231 addr += 1;
232
233 let mut cursor = 0;
235 let mut table_cursors = std::collections::HashMap::new();
236
237 for node in &scan_nodes {
238 if let Some(ref table_name) = node.object {
239 if !table_cursors.contains_key(table_name) {
240 let root_page = 2 + cursor; instructions.push(VmInstruction {
242 addr,
243 opcode: "OpenRead".to_string(),
244 p1: cursor,
245 p2: root_page,
246 p3: 0,
247 p4: "2".to_string(), p5: 0,
249 comment: format!("root={} iDb=0; {}", root_page, table_name),
250 });
251 table_cursors.insert(table_name.clone(), cursor);
252 cursor += 1;
253 addr += 1;
254 }
255 }
256 }
257
258 let result_row_addr = addr + scan_nodes.len() * 2 + 1;
260 let halt_addr = result_row_addr + 1;
261
262 for node in &scan_nodes {
263 if let Some(ref table_name) = node.object {
264 let cursor_id = *table_cursors.get(table_name).unwrap_or(&0);
265
266 match node.scan_type.as_ref() {
267 Some(ScanType::Search) | Some(ScanType::CoveringIndex) => {
268 instructions.push(VmInstruction {
270 addr,
271 opcode: "SeekGE".to_string(),
272 p1: cursor_id,
273 p2: halt_addr as i32,
274 p3: 1,
275 p4: String::new(),
276 p5: 0,
277 comment: format!("key=r[1]; {}", table_name),
278 });
279 addr += 1;
280 }
281 Some(ScanType::IntegerPrimaryKey) => {
282 instructions.push(VmInstruction {
284 addr,
285 opcode: "SeekRowid".to_string(),
286 p1: cursor_id,
287 p2: halt_addr as i32,
288 p3: 1,
289 p4: String::new(),
290 p5: 0,
291 comment: format!("pk; {}", table_name),
292 });
293 addr += 1;
294 }
295 Some(ScanType::Scan) | None => {
296 instructions.push(VmInstruction {
298 addr,
299 opcode: "Rewind".to_string(),
300 p1: cursor_id,
301 p2: halt_addr as i32,
302 p3: 0,
303 p4: String::new(),
304 p5: 0,
305 comment: table_name.clone(),
306 });
307 addr += 1;
308 }
309 }
310 }
311 }
312
313 let mut register = 1;
315 for (i, node) in scan_nodes.iter().enumerate() {
316 if let Some(ref table_name) = node.object {
317 let cursor_id = *table_cursors.get(table_name).unwrap_or(&0);
318 instructions.push(VmInstruction {
320 addr,
321 opcode: "Column".to_string(),
322 p1: cursor_id,
323 p2: i as i32, p3: register,
325 p4: String::new(),
326 p5: 0,
327 comment: format!("r[{}]=cursor {} column {}", register, cursor_id, i),
328 });
329 register += 1;
330 addr += 1;
331 }
332 }
333
334 instructions.push(VmInstruction {
336 addr,
337 opcode: "ResultRow".to_string(),
338 p1: 1,
339 p2: register - 1,
340 p3: 0,
341 p4: String::new(),
342 p5: 0,
343 comment: format!("output=r[1..{}]", register - 1),
344 });
345 addr += 1;
346
347 for node in &scan_nodes {
349 if let Some(ref table_name) = node.object {
350 let cursor_id = *table_cursors.get(table_name).unwrap_or(&0);
351 let seek_addr = scan_nodes
352 .iter()
353 .position(|n| n.object.as_ref() == Some(table_name))
354 .map(|pos| 1 + table_cursors.len() + pos)
355 .unwrap_or(1);
356
357 instructions.push(VmInstruction {
358 addr,
359 opcode: "Next".to_string(),
360 p1: cursor_id,
361 p2: seek_addr as i32,
362 p3: 0,
363 p4: String::new(),
364 p5: 0,
365 comment: table_name.clone(),
366 });
367 addr += 1;
368 }
369 }
370
371 instructions.push(VmInstruction {
373 addr,
374 opcode: "Halt".to_string(),
375 p1: 0,
376 p2: 0,
377 p3: 0,
378 p4: String::new(),
379 p5: 0,
380 comment: String::new(),
381 });
382 addr += 1;
383
384 let transaction_addr = addr;
386 instructions.push(VmInstruction {
387 addr,
388 opcode: "Transaction".to_string(),
389 p1: 0,
390 p2: 0,
391 p3: 1,
392 p4: "0".to_string(),
393 p5: 1,
394 comment: "usesStmtJournal=0".to_string(),
395 });
396 addr += 1;
397
398 instructions.push(VmInstruction {
400 addr,
401 opcode: "Goto".to_string(),
402 p1: 0,
403 p2: 1,
404 p3: 0,
405 p4: String::new(),
406 p5: 0,
407 comment: String::new(),
408 });
409
410 if let Some(init) = instructions.get_mut(init_addr) {
412 init.p2 = transaction_addr as i32;
413 init.comment = format!("Start at {}", transaction_addr);
414 }
415
416 SqliteVmOutput { instructions }
417 }
418}
419
420#[derive(Debug, Clone)]
422pub struct VmInstruction {
423 pub addr: usize,
425 pub opcode: String,
427 pub p1: i32,
429 pub p2: i32,
431 pub p3: i32,
433 pub p4: String,
435 pub p5: i32,
437 pub comment: String,
439}
440
441#[derive(Debug)]
443pub struct SqliteVmOutput {
444 pub instructions: Vec<VmInstruction>,
446}
447
448impl SqliteVmOutput {
449 pub fn column_names() -> Vec<&'static str> {
451 vec!["addr", "opcode", "p1", "p2", "p3", "p4", "p5", "comment"]
452 }
453
454 pub fn to_rows(&self) -> Vec<Vec<String>> {
456 self.instructions
457 .iter()
458 .map(|inst| {
459 vec![
460 inst.addr.to_string(),
461 inst.opcode.clone(),
462 inst.p1.to_string(),
463 inst.p2.to_string(),
464 inst.p3.to_string(),
465 inst.p4.clone(),
466 inst.p5.to_string(),
467 inst.comment.clone(),
468 ]
469 })
470 .collect()
471 }
472}
473
474fn collect_scan_nodes(node: &PlanNode) -> Vec<&PlanNode> {
476 let mut nodes = Vec::new();
477
478 if node.scan_type.is_some() {
480 nodes.push(node);
481 }
482
483 for child in &node.children {
485 nodes.extend(collect_scan_nodes(child));
486 }
487
488 nodes
489}
490
491fn collect_eqp_entries(node: &PlanNode) -> Vec<String> {
493 let mut entries = Vec::new();
494
495 let scan_nodes = collect_scan_nodes(node);
497 for scan_node in &scan_nodes {
498 entries.push(format_sqlite_eqp_node(scan_node));
499 }
500
501 if node.needs_temp_btree_for_order_by {
503 entries.push("USE TEMP B-TREE FOR ORDER BY".to_string());
504 }
505
506 for child in &node.children {
508 if child.needs_temp_btree_for_order_by && !node.needs_temp_btree_for_order_by {
509 entries.push("USE TEMP B-TREE FOR ORDER BY".to_string());
510 break;
511 }
512 }
513
514 entries
515}
516
517fn format_compound_query_eqp(node: &PlanNode, indent: &str, is_last: bool, output: &mut String) {
519 let connector = if is_last { "`--" } else { "|--" };
520 let child_indent = if is_last {
521 format!("{} ", indent)
522 } else {
523 format!("{}| ", indent)
524 };
525
526 writeln!(output, "{}{}COMPOUND QUERY", indent, connector).unwrap();
528
529 let child_count = node.children.len();
531 for (i, child) in node.children.iter().enumerate() {
532 let is_child_last = i == child_count - 1;
533 let child_connector = if is_child_last { "`--" } else { "|--" };
534 let grandchild_indent = if is_child_last {
535 format!("{} ", child_indent)
536 } else {
537 format!("{}| ", child_indent)
538 };
539
540 if i == 0 {
541 writeln!(output, "{}{}LEFT-MOST SUBQUERY", child_indent, child_connector).unwrap();
543 let scan_nodes = collect_scan_nodes(child);
545 for (j, scan_node) in scan_nodes.iter().enumerate() {
546 let is_scan_last = j == scan_nodes.len() - 1;
547 let scan_connector = if is_scan_last { "`--" } else { "|--" };
548 writeln!(output, "{}{}{}", grandchild_indent, scan_connector, format_sqlite_eqp_node(scan_node)).unwrap();
549 }
550 } else {
551 let set_op = child.set_operation_type.as_deref().unwrap_or("UNION ALL");
553 writeln!(output, "{}{}{}", child_indent, child_connector, set_op).unwrap();
554 let scan_nodes = collect_scan_nodes(child);
556 for (j, scan_node) in scan_nodes.iter().enumerate() {
557 let is_scan_last = j == scan_nodes.len() - 1;
558 let scan_connector = if is_scan_last { "`--" } else { "|--" };
559 writeln!(output, "{}{}{}", grandchild_indent, scan_connector, format_sqlite_eqp_node(scan_node)).unwrap();
560 }
561 }
562 }
563}
564
565fn format_sqlite_eqp_node(node: &PlanNode) -> String {
567 if node.operation == "SCAN CONSTANT ROW" {
569 return "SCAN CONSTANT ROW".to_string();
570 }
571
572 let table_name = node.object.as_deref().unwrap_or("?");
573
574 match node.scan_type.as_ref() {
575 Some(ScanType::Scan) => {
576 if let Some(ref index_name) = node.index_name {
578 format!("SCAN {} USING INDEX {}", table_name, index_name)
579 } else {
580 format!("SCAN {}", table_name)
581 }
582 }
583 Some(ScanType::Search) | Some(ScanType::CoveringIndex) => {
584 let index_name = node.index_name.as_deref().unwrap_or("?");
585 let covering = if matches!(node.scan_type, Some(ScanType::CoveringIndex)) {
586 "COVERING "
587 } else {
588 ""
589 };
590
591 if node.index_predicates.is_empty() {
592 format!("SEARCH {} USING {}INDEX {}", table_name, covering, index_name)
593 } else {
594 let predicates: Vec<String> = node
595 .index_predicates
596 .iter()
597 .map(|p| format!("{}{}?", p.column, p.predicate_type))
598 .collect();
599 format!(
600 "SEARCH {} USING {}INDEX {} ({})",
601 table_name,
602 covering,
603 index_name,
604 predicates.join(" AND ")
605 )
606 }
607 }
608 Some(ScanType::IntegerPrimaryKey) => {
609 if node.index_predicates.is_empty() {
610 format!("SEARCH {} USING INTEGER PRIMARY KEY (rowid=?)", table_name)
611 } else {
612 let predicates: Vec<String> = node
613 .index_predicates
614 .iter()
615 .map(|p| format!("rowid{}?", p.predicate_type))
616 .collect();
617 format!(
618 "SEARCH {} USING INTEGER PRIMARY KEY ({})",
619 table_name,
620 predicates.join(" AND ")
621 )
622 }
623 }
624 None => {
625 node.operation.clone()
627 }
628 }
629}
630
631fn format_node_text(node: &PlanNode, depth: usize, output: &mut String) {
632 let indent = " ".repeat(depth);
633 let arrow = if depth > 0 { "-> " } else { "" };
634
635 let mut line = format!("{}{}{}", indent, arrow, node.operation);
637
638 if let Some(ref obj) = node.object {
639 write!(line, " on {}", obj).unwrap();
640 }
641
642 if let Some(rows) = node.estimated_rows {
643 write!(line, " (rows={:.0})", rows).unwrap();
644 }
645
646 writeln!(output, "{}", line).unwrap();
647
648 for detail in &node.details {
650 writeln!(output, "{} {}", indent, detail).unwrap();
651 }
652
653 for child in &node.children {
655 format_node_text(child, depth + 1, output);
656 }
657}
658
659fn format_node_json(node: &PlanNode) -> String {
660 let mut parts = vec![format!("\"operation\": \"{}\"", node.operation)];
661
662 if let Some(ref obj) = node.object {
663 parts.push(format!("\"object\": \"{}\"", obj));
664 }
665
666 if !node.details.is_empty() {
667 let details: Vec<String> = node.details.iter().map(|d| format!("\"{}\"", d)).collect();
668 parts.push(format!("\"details\": [{}]", details.join(", ")));
669 }
670
671 if let Some(rows) = node.estimated_rows {
672 parts.push(format!("\"estimated_rows\": {:.0}", rows));
673 }
674
675 if !node.children.is_empty() {
676 let children: Vec<String> = node.children.iter().map(format_node_json).collect();
677 parts.push(format!("\"children\": [{}]", children.join(", ")));
678 }
679
680 format!("{{{}}}", parts.join(", "))
681}
682
683pub struct ExplainExecutor;
685
686impl ExplainExecutor {
687 pub fn execute(
689 stmt: &ExplainStmt,
690 database: &Database,
691 ) -> Result<ExplainResult, ExecutorError> {
692 let plan = match stmt.statement.as_ref() {
693 Statement::Select(select_stmt) => Self::explain_select(select_stmt, database)?,
694 Statement::Insert(_) => {
695 PlanNode::new("Insert").with_detail("Inserts rows into target table".to_string())
696 }
697 Statement::Update(_) => PlanNode::new("Update")
698 .with_detail("Updates rows matching WHERE clause".to_string()),
699 Statement::Delete(_) => PlanNode::new("Delete")
700 .with_detail("Deletes rows matching WHERE clause".to_string()),
701 _ => {
702 return Err(ExecutorError::Other(
703 "EXPLAIN only supports SELECT, INSERT, UPDATE, DELETE statements".to_string(),
704 ));
705 }
706 };
707
708 Ok(ExplainResult { plan, format: stmt.format.clone() })
709 }
710
711 fn explain_select(stmt: &SelectStmt, database: &Database) -> Result<PlanNode, ExecutorError> {
713 if stmt.set_operation.is_some() {
715 return Self::explain_compound_select(stmt, database);
716 }
717
718 let mut root = PlanNode::new("Select");
719
720 let needed_columns = extract_select_columns(&stmt.select_list);
722
723 if let Some(ref from_clause) = stmt.from {
725 let scan_node = Self::explain_from_clause(
726 from_clause,
727 &stmt.where_clause,
728 &stmt.order_by,
729 &needed_columns,
730 database,
731 )?;
732 root.add_child(scan_node);
733 } else {
734 let mut constant_node = PlanNode::new("Constant Row");
736 constant_node.scan_type = Some(ScanType::Scan);
737 constant_node.operation = "SCAN CONSTANT ROW".to_string();
738 root.add_child(constant_node);
739 }
740
741 if let Some(ref order_by) = stmt.order_by {
744 let needs_temp = Self::needs_temp_btree_for_order_by(
745 stmt.from.as_ref(),
746 order_by,
747 database,
748 );
749 root.needs_temp_btree_for_order_by = needs_temp;
750 }
751
752 if stmt.where_clause.is_some() {
754 root.details.push("Filter: <where clause>".to_string());
755 }
756
757 if stmt.group_by.is_some() {
759 root.details.push("Group: <group by clause>".to_string());
760 }
761
762 if stmt.order_by.is_some() {
764 root.details.push("Sort: <order by clause>".to_string());
765 }
766
767 if let Some(ref limit) = stmt.limit {
769 root.details.push(format!("Limit: {}", limit.to_sql()));
770 }
771
772 Ok(root)
773 }
774
775 fn explain_compound_select(stmt: &SelectStmt, database: &Database) -> Result<PlanNode, ExecutorError> {
777 let mut root = PlanNode::new("CompoundQuery");
778 root.is_compound_query = true;
779
780 let left_stmt = SelectStmt {
782 with_clause: stmt.with_clause.clone(),
783 distinct: stmt.distinct,
784 select_list: stmt.select_list.clone(),
785 into_table: stmt.into_table.clone(),
786 into_variables: stmt.into_variables.clone(),
787 from: stmt.from.clone(),
788 where_clause: stmt.where_clause.clone(),
789 group_by: stmt.group_by.clone(),
790 having: stmt.having.clone(),
791 order_by: None, limit: None,
793 offset: None,
794 set_operation: None,
795 values: stmt.values.clone(),
796 };
797 let left_plan = Self::explain_select(&left_stmt, database)?;
798 root.add_child(left_plan);
799
800 let mut current_set_op = stmt.set_operation.as_ref();
802 while let Some(set_op) = current_set_op {
803 let op_label = match (&set_op.op, set_op.all) {
804 (vibesql_ast::SetOperator::Union, true) => "UNION ALL",
805 (vibesql_ast::SetOperator::Union, false) => "UNION",
806 (vibesql_ast::SetOperator::Intersect, true) => "INTERSECT ALL",
807 (vibesql_ast::SetOperator::Intersect, false) => "INTERSECT",
808 (vibesql_ast::SetOperator::Except, true) => "EXCEPT ALL",
809 (vibesql_ast::SetOperator::Except, false) => "EXCEPT",
810 };
811
812 let right_stmt = SelectStmt {
814 with_clause: set_op.right.with_clause.clone(),
815 distinct: set_op.right.distinct,
816 select_list: set_op.right.select_list.clone(),
817 into_table: set_op.right.into_table.clone(),
818 into_variables: set_op.right.into_variables.clone(),
819 from: set_op.right.from.clone(),
820 where_clause: set_op.right.where_clause.clone(),
821 group_by: set_op.right.group_by.clone(),
822 having: set_op.right.having.clone(),
823 order_by: None,
824 limit: None,
825 offset: None,
826 set_operation: None,
827 values: set_op.right.values.clone(),
828 };
829 let mut right_plan = Self::explain_select(&right_stmt, database)?;
830 right_plan.set_operation_type = Some(op_label.to_string());
831 root.add_child(right_plan);
832
833 current_set_op = set_op.right.set_operation.as_ref();
835 }
836
837 Ok(root)
838 }
839
840 fn needs_temp_btree_for_order_by(
842 from: Option<&vibesql_ast::FromClause>,
843 order_by: &[vibesql_ast::OrderByItem],
844 database: &Database,
845 ) -> bool {
846 let Some(from_clause) = from else {
849 return false;
850 };
851
852 let table_name = match from_clause {
854 vibesql_ast::FromClause::Table { name, .. } => name.as_str(),
855 _ => return true, };
857
858 let order_by_vec: Vec<vibesql_ast::OrderByItem> = order_by.to_vec();
860 let index_info = cost_based_index_selection(
861 table_name,
862 None, Some(&order_by_vec),
864 database,
865 );
866
867 if index_info.is_none() {
868 return true; }
870
871 if let Some((_index_name, sorted_cols)) = index_info {
873 if let Some(ref cols) = sorted_cols {
874 if cols.len() < order_by.len() {
876 return true;
877 }
878 } else {
879 return true;
881 }
882 }
883
884 false
885 }
886
887 fn explain_from_clause(
889 from: &vibesql_ast::FromClause,
890 where_clause: &Option<vibesql_ast::Expression>,
891 order_by: &Option<Vec<vibesql_ast::OrderByItem>>,
892 needed_columns: &HashSet<String>,
893 database: &Database,
894 ) -> Result<PlanNode, ExecutorError> {
895 match from {
896 vibesql_ast::FromClause::Table { name, alias, .. } => {
897 Self::explain_table_scan(
898 name,
899 alias.as_deref(),
900 where_clause,
901 order_by,
902 needed_columns,
903 database,
904 )
905 }
906 vibesql_ast::FromClause::Join {
907 left,
908 right,
909 join_type,
910 condition,
911 using_columns,
912 natural,
913 ..
914 } => {
915 let join_name = match join_type {
916 vibesql_ast::JoinType::Inner => "Inner Join",
917 vibesql_ast::JoinType::LeftOuter => "Left Outer Join",
918 vibesql_ast::JoinType::RightOuter => "Right Outer Join",
919 vibesql_ast::JoinType::FullOuter => "Full Outer Join",
920 vibesql_ast::JoinType::Cross => "Cross Join",
921 vibesql_ast::JoinType::Semi => "Semi Join",
922 vibesql_ast::JoinType::Anti => "Anti Join",
923 };
924
925 let mut join_node = PlanNode::new(join_name);
926
927 if *natural {
928 join_node.details.push("NATURAL join".to_string());
929 }
930
931 if condition.is_some() {
932 join_node.details.push("Join condition: <on clause>".to_string());
933 }
934
935 if let Some(cols) = using_columns {
936 join_node.details.push(format!("USING ({})", cols.join(", ")));
937 }
938
939 let left_child =
941 Self::explain_from_clause(left, where_clause, order_by, needed_columns, database)?;
942 join_node.add_child(left_child);
943
944 let empty_cols = HashSet::new();
946 let right_child =
947 Self::explain_from_clause(right, &None, &None, &empty_cols, database)?;
948 join_node.add_child(right_child);
949
950 Ok(join_node)
951 }
952 vibesql_ast::FromClause::Subquery { query, alias, .. } => {
953 let mut subquery_node = PlanNode::new("Subquery");
954 subquery_node.object = Some(format!("AS {}", alias));
955
956 let child = Self::explain_select(query, database)?;
957 subquery_node.add_child(child);
958
959 Ok(subquery_node)
960 }
961 vibesql_ast::FromClause::Values { rows, alias, column_aliases } => {
962 let mut values_node = PlanNode::new("Values");
963 values_node.object = Some(format!("AS {}", alias));
964
965 values_node.details.push(format!("{} row(s)", rows.len()));
966
967 if let Some(aliases) = column_aliases {
968 values_node.details.push(format!("Columns: {}", aliases.join(", ")));
969 }
970
971 Ok(values_node)
972 }
973 }
974 }
975
976 fn explain_table_scan(
978 table_name: &str,
979 alias: Option<&str>,
980 where_clause: &Option<vibesql_ast::Expression>,
981 order_by: &Option<Vec<vibesql_ast::OrderByItem>>,
982 needed_columns: &HashSet<String>,
983 database: &Database,
984 ) -> Result<PlanNode, ExecutorError> {
985 let index_info = cost_based_index_selection(
987 table_name,
988 where_clause.as_ref(),
989 order_by.as_ref().map(|v| v.as_slice()),
990 database,
991 );
992
993 let skip_scan_plan = if index_info.is_none() {
995 if let Some(where_expr) = where_clause {
996 let planner = IndexPlanner::new(database);
997 planner.plan_skip_scan(table_name, where_expr)
998 } else {
999 None
1000 }
1001 } else {
1002 None
1003 };
1004
1005 let is_pk_lookup = Self::is_primary_key_lookup(table_name, where_clause, database);
1007
1008 let mut node = if let Some(skip_plan) = skip_scan_plan {
1009 let skip_info = skip_plan.skip_scan_info.as_ref().unwrap();
1011
1012 let mut skip_node = PlanNode::new("Skip Scan")
1013 .with_object(table_name)
1014 .with_scan_type(ScanType::Search)
1015 .with_index_name(&skip_plan.index_name);
1016 skip_node.details.push(format!("USING INDEX {} ", skip_plan.index_name));
1017 skip_node.details.push(format!(
1018 "Skip columns: {} (cardinality: {})",
1019 skip_info.prefix_columns.join(", "),
1020 skip_info.prefix_cardinality
1021 ));
1022 skip_node.details.push(format!("Filter column: {}", skip_info.filter_column));
1023 skip_node.details.push(format!("Estimated cost: {:.2}", skip_info.estimated_cost));
1024
1025 skip_node = skip_node.with_index_predicate(&skip_info.filter_column, "=");
1027
1028 skip_node
1029 } else if let Some((index_name, sorted_cols)) = index_info {
1030 let is_covering = if needed_columns.is_empty() {
1039 false
1041 } else {
1042 let mut all_needed_columns = needed_columns.clone();
1043 if let Some(where_expr) = where_clause {
1044 collect_column_refs(where_expr, &mut all_needed_columns);
1045 }
1046 is_covering_index(&index_name, &all_needed_columns, database)
1047 };
1048
1049 let has_index_predicates = if let Some(where_expr) = where_clause {
1051 let predicates = extract_index_predicates(where_expr, &index_name, database);
1052 !predicates.is_empty()
1053 } else {
1054 false
1055 };
1056
1057 let scan_type = if is_pk_lookup {
1061 ScanType::IntegerPrimaryKey
1062 } else if has_index_predicates {
1063 if is_covering {
1064 ScanType::CoveringIndex
1065 } else {
1066 ScanType::Search
1067 }
1068 } else {
1069 ScanType::Scan
1071 };
1072
1073 let mut idx_node = PlanNode::new("Index Scan")
1074 .with_object(table_name)
1075 .with_scan_type(scan_type.clone())
1076 .with_index_name(&index_name);
1077 idx_node.details.push(format!("USING INDEX {} ", index_name));
1078
1079 if let Some(where_expr) = where_clause {
1081 let predicates = extract_index_predicates(where_expr, &index_name, database);
1082 for (col, op) in predicates {
1083 idx_node = idx_node.with_index_predicate(&col, &op);
1084 }
1085 }
1086
1087 if let Some(cols) = sorted_cols {
1088 let col_strs: Vec<String> = cols
1089 .iter()
1090 .map(|(col, dir)| {
1091 format!(
1092 "{} {}",
1093 col,
1094 match dir {
1095 vibesql_ast::OrderDirection::Asc => "ASC",
1096 vibesql_ast::OrderDirection::Desc => "DESC",
1097 }
1098 )
1099 })
1100 .collect();
1101 idx_node.details.push(format!("Sorted by: {}", col_strs.join(", ")));
1102 }
1103
1104 idx_node
1105 } else {
1106 PlanNode::new("Seq Scan").with_object(table_name).with_scan_type(ScanType::Scan)
1107 };
1108
1109 if let Some(a) = alias {
1111 node.details.push(format!("Alias: {}", a));
1112 }
1113
1114 if let Some(table) = database.get_table(table_name) {
1116 let row_count = table.row_count();
1117 node = node.with_estimated_rows(row_count as f64);
1118 }
1119
1120 Ok(node)
1121 }
1122
1123 fn is_primary_key_lookup(
1125 table_name: &str,
1126 where_clause: &Option<Expression>,
1127 database: &Database,
1128 ) -> bool {
1129 let Some(table) = database.get_table(table_name) else {
1130 return false;
1131 };
1132
1133 let Some(pk_columns) = table.schema.primary_key.as_ref() else {
1134 return false;
1135 };
1136
1137 if pk_columns.len() != 1 {
1139 return false;
1140 }
1141
1142 let pk_col = &pk_columns[0];
1143 let pk_col_lower = pk_col.to_lowercase();
1144
1145 if let Some(where_expr) = where_clause {
1147 return expression_references_column(where_expr, &pk_col_lower);
1148 }
1149
1150 false
1151 }
1152}
1153
1154fn expression_references_column(expr: &Expression, column: &str) -> bool {
1156 match expr {
1157 Expression::ColumnRef(col_id) => col_id.column_canonical().to_lowercase() == column,
1158 Expression::BinaryOp { left, right, .. } => {
1159 expression_references_column(left, column)
1160 || expression_references_column(right, column)
1161 }
1162 Expression::Conjunction(exprs) | Expression::Disjunction(exprs) => {
1163 exprs.iter().any(|e| expression_references_column(e, column))
1164 }
1165 Expression::UnaryOp { expr: inner, .. } => expression_references_column(inner, column),
1166 Expression::IsNull { expr: inner, .. } => expression_references_column(inner, column),
1167 Expression::Between { expr, low, high, .. } => {
1168 expression_references_column(expr, column)
1169 || expression_references_column(low, column)
1170 || expression_references_column(high, column)
1171 }
1172 Expression::InList { expr, values, .. } => {
1173 expression_references_column(expr, column)
1174 || values.iter().any(|e| expression_references_column(e, column))
1175 }
1176 _ => false,
1177 }
1178}
1179
1180fn extract_select_columns(select_list: &[SelectItem]) -> HashSet<String> {
1182 let mut columns = HashSet::new();
1183 for item in select_list {
1184 match item {
1185 SelectItem::Wildcard { .. } => {
1186 return HashSet::new();
1189 }
1190 SelectItem::QualifiedWildcard { .. } => {
1191 return HashSet::new();
1193 }
1194 SelectItem::Expression { expr, .. } => {
1195 collect_column_refs(expr, &mut columns);
1196 }
1197 }
1198 }
1199 columns
1200}
1201
1202fn collect_column_refs(expr: &Expression, columns: &mut HashSet<String>) {
1204 match expr {
1205 Expression::ColumnRef(col_id) => {
1206 columns.insert(col_id.column_canonical().to_lowercase());
1207 }
1208 Expression::BinaryOp { left, right, .. } => {
1209 collect_column_refs(left, columns);
1210 collect_column_refs(right, columns);
1211 }
1212 Expression::UnaryOp { expr: inner, .. } => {
1213 collect_column_refs(inner, columns);
1214 }
1215 Expression::Conjunction(exprs) | Expression::Disjunction(exprs) => {
1216 for e in exprs {
1217 collect_column_refs(e, columns);
1218 }
1219 }
1220 Expression::Function { args, .. } | Expression::AggregateFunction { args, .. } => {
1221 for arg in args {
1222 collect_column_refs(arg, columns);
1223 }
1224 }
1225 Expression::IsNull { expr: inner, .. } => {
1226 collect_column_refs(inner, columns);
1227 }
1228 Expression::Between { expr, low, high, .. } => {
1229 collect_column_refs(expr, columns);
1230 collect_column_refs(low, columns);
1231 collect_column_refs(high, columns);
1232 }
1233 Expression::InList { expr, values, .. } => {
1234 collect_column_refs(expr, columns);
1235 for v in values {
1236 collect_column_refs(v, columns);
1237 }
1238 }
1239 Expression::Case { operand, when_clauses, else_result, .. } => {
1240 if let Some(op) = operand {
1241 collect_column_refs(op, columns);
1242 }
1243 for case_when in when_clauses {
1244 for cond in &case_when.conditions {
1245 collect_column_refs(cond, columns);
1246 }
1247 collect_column_refs(&case_when.result, columns);
1248 }
1249 if let Some(else_res) = else_result {
1250 collect_column_refs(else_res, columns);
1251 }
1252 }
1253 Expression::Cast { expr: inner, .. } => {
1254 collect_column_refs(inner, columns);
1255 }
1256 Expression::ScalarSubquery(_)
1257 | Expression::In { .. }
1258 | Expression::Literal(_)
1259 | Expression::Placeholder(_)
1260 | Expression::NumberedPlaceholder(_)
1261 | Expression::NamedPlaceholder(_)
1262 | Expression::Wildcard => {}
1263 _ => {} }
1265}
1266
1267fn is_covering_index(
1269 index_name: &str,
1270 needed_columns: &HashSet<String>,
1271 database: &Database,
1272) -> bool {
1273 if needed_columns.is_empty() {
1275 return false;
1276 }
1277
1278 let Some(index) = database.get_index(index_name) else {
1279 return false;
1280 };
1281
1282 let index_columns: HashSet<String> = index
1285 .columns
1286 .iter()
1287 .filter_map(|c| c.column_name())
1288 .map(|name| name.to_lowercase())
1289 .collect();
1290
1291 needed_columns.iter().all(|col| index_columns.contains(col))
1293}
1294
1295fn extract_index_predicates(
1302 expr: &Expression,
1303 index_name: &str,
1304 database: &Database,
1305) -> Vec<(String, String)> {
1306 use vibesql_ast::pretty_print::ToSql;
1307
1308 let mut predicates = Vec::new();
1309
1310 let index_columns: Vec<String> = database
1313 .get_index(index_name)
1314 .map(|idx| {
1315 idx.columns
1316 .iter()
1317 .map(|c| {
1318 if let Some(name) = c.column_name() {
1319 name.to_lowercase()
1320 } else if let Some(expr) = c.get_expression() {
1321 expr.to_sql().to_lowercase()
1323 } else {
1324 String::new()
1325 }
1326 })
1327 .collect()
1328 })
1329 .unwrap_or_default();
1330
1331 extract_predicates_recursive(expr, &index_columns, &mut predicates);
1332
1333 predicates.sort_by_key(|(col, _)| {
1335 let col_lower = col.to_lowercase();
1336 index_columns.iter().position(|c| c == &col_lower).unwrap_or(usize::MAX)
1337 });
1338
1339 predicates
1340}
1341
1342fn extract_predicates_recursive(
1343 expr: &Expression,
1344 index_columns: &[String],
1345 predicates: &mut Vec<(String, String)>,
1346) {
1347 use vibesql_ast::pretty_print::ToSql;
1348 use vibesql_ast::BinaryOperator;
1349
1350 fn expr_matches_index(expr: &Expression, index_columns: &[String]) -> Option<String> {
1352 match expr {
1353 Expression::ColumnRef(col_id) => {
1354 let col_name = col_id.column_canonical().to_lowercase();
1355 if index_columns.iter().any(|c| c == &col_name) {
1356 Some(col_id.column_canonical().to_string())
1357 } else {
1358 None
1359 }
1360 }
1361 Expression::Function { .. } | Expression::BinaryOp { .. } => {
1363 let expr_str = expr.to_sql().to_lowercase();
1364 if index_columns.iter().any(|c| c == &expr_str) {
1365 Some(expr.to_sql())
1366 } else {
1367 None
1368 }
1369 }
1370 _ => None,
1371 }
1372 }
1373
1374 match expr {
1375 Expression::BinaryOp { op, left, right } => {
1376 let op_str = match op {
1378 BinaryOperator::Equal => Some("="),
1379 BinaryOperator::NotEqual => None, BinaryOperator::LessThan => Some("<"),
1381 BinaryOperator::LessThanOrEqual => Some("<="),
1382 BinaryOperator::GreaterThan => Some(">"),
1383 BinaryOperator::GreaterThanOrEqual => Some(">="),
1384 BinaryOperator::And => {
1385 extract_predicates_recursive(left, index_columns, predicates);
1387 extract_predicates_recursive(right, index_columns, predicates);
1388 return;
1389 }
1390 _ => None,
1391 };
1392
1393 if let Some(op_str) = op_str {
1394 if let Some(expr_name) = expr_matches_index(left, index_columns) {
1396 predicates.push((expr_name, op_str.to_string()));
1397 }
1398 else if let Some(expr_name) = expr_matches_index(right, index_columns) {
1400 let reversed_op = match op {
1402 BinaryOperator::Equal => "=",
1403 BinaryOperator::LessThan => ">",
1404 BinaryOperator::LessThanOrEqual => ">=",
1405 BinaryOperator::GreaterThan => "<",
1406 BinaryOperator::GreaterThanOrEqual => "<=",
1407 _ => return,
1408 };
1409 predicates.push((expr_name, reversed_op.to_string()));
1410 }
1411 }
1412 }
1413 Expression::Conjunction(exprs) => {
1414 for e in exprs {
1415 extract_predicates_recursive(e, index_columns, predicates);
1416 }
1417 }
1418 Expression::Between { expr, negated: false, .. } => {
1419 if let Expression::ColumnRef(col_id) = expr.as_ref() {
1421 let col_name = col_id.column_canonical().to_lowercase();
1422 if index_columns.iter().any(|c| c == &col_name) {
1423 let col = col_id.column_canonical().to_string();
1424 predicates.push((col.clone(), ">=".to_string()));
1425 predicates.push((col, "<=".to_string()));
1426 }
1427 }
1428 }
1429 Expression::InList { expr, negated: false, .. } => {
1430 if let Expression::ColumnRef(col_id) = expr.as_ref() {
1432 let col_name = col_id.column_canonical().to_lowercase();
1433 if index_columns.iter().any(|c| c == &col_name) {
1434 predicates.push((col_id.column_canonical().to_string(), "=".to_string()));
1435 }
1436 }
1437 }
1438 Expression::IsDistinctFrom { left, right, negated: true } => {
1441 if let Expression::ColumnRef(col_id) = left.as_ref() {
1443 let col_name = col_id.column_canonical().to_lowercase();
1444 if index_columns.iter().any(|c| c == &col_name) {
1445 predicates.push((col_id.column_canonical().to_string(), "=".to_string()));
1446 }
1447 }
1448 else if let Expression::ColumnRef(col_id) = right.as_ref() {
1450 let col_name = col_id.column_canonical().to_lowercase();
1451 if index_columns.iter().any(|c| c == &col_name) {
1452 predicates.push((col_id.column_canonical().to_string(), "=".to_string()));
1453 }
1454 }
1455 }
1456 _ => {}
1457 }
1458}