1use super::describe;
17use super::*;
18use crate::bind::{BoundOrderTerm, SubqueryKind};
19
20#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct PlanLine {
23 pub depth: u16,
25 pub detail: String,
27}
28
29fn push(out: &mut Vec<PlanLine>, depth: u16, detail: impl Into<String>) {
35 out.push(PlanLine {
36 depth,
37 detail: detail.into(),
38 });
39}
40
41pub(super) fn tree_of(plan: &PhysicalPlan, depth: u16, out: &mut Vec<PlanLine>) {
47 if !plan.compounds.is_empty() {
48 compound_tree(plan, depth, out);
49 return;
50 }
51 if !plan.select.windows.is_empty() {
52 window_tree(plan, depth, out);
53 return;
54 }
55 select_tree(plan, depth, out);
56}
57
58fn select_tree(plan: &PhysicalPlan, depth: u16, out: &mut Vec<PlanLine>) {
65 if let Some(inner) = flattened_compound(plan) {
66 tree_of(inner, depth, out);
67 return;
68 }
69 derived_nodes(plan, depth, out);
70 if plan.sources.is_empty() {
71 let rows = plan.select.values.len();
72 if rows > 1 {
73 push(out, depth, format!("SCAN {rows}-ROW VALUES CLAUSE"));
74 } else {
75 push(out, depth, "SCAN CONSTANT ROW");
76 }
77 }
78 let extreme = min_or_max_search(plan);
79 for (position, line) in describe::loop_lines(plan).into_iter().enumerate() {
80 match (&extreme, position) {
81 (Some(search), 0) => push(out, depth, search.clone()),
82 _ => push(out, depth, line),
83 }
84 }
85 expression_subqueries(plan, depth, out);
86 for line in describe::temp_lines(plan) {
87 push(out, depth, line);
88 }
89}
90
91fn flattened_compound(plan: &PhysicalPlan) -> Option<&PhysicalPlan> {
105 let select = &plan.select;
106 let [only] = select.sources.as_slice() else {
107 return None;
108 };
109 let SourceRows::Subquery(block) = &only.rows else {
110 return None;
111 };
112 let simple = |arm: &BoundSelect| {
113 arm.aggregates.is_empty()
114 && arm.group_by.is_empty()
115 && !arm.distinct
116 && arm.windows.is_empty()
117 && !arm.sources.is_empty()
118 };
119 if block.compounds.is_empty()
120 || !block
121 .compounds
122 .iter()
123 .all(|(op, arm)| *op == CompoundOp::UnionAll && simple(arm))
124 || !simple(block)
125 || !block.order_by.is_empty()
126 || block.limit.is_some()
127 || only.derived.materialized == Some(true)
128 || !select.aggregates.is_empty()
129 || !select.group_by.is_empty()
130 || select.distinct
131 || !select.windows.is_empty()
132 || !select.order_by.is_empty()
133 || select.limit.is_some()
134 {
135 return None;
136 }
137 match &plan.sources.first()?.path {
138 AccessPath::Subquery { plan: inner, .. } => Some(inner),
139 _ => None,
140 }
141}
142
143fn min_or_max_search(plan: &PhysicalPlan) -> Option<String> {
153 let select = &plan.select;
154 let [aggregate] = select.aggregates.as_slice() else {
155 return None;
156 };
157 let extreme = matches!(
158 aggregate.func,
159 crate::function::AggregateFunc::Min | crate::function::AggregateFunc::Max
160 );
161 let [only] = plan.sources.as_slice() else {
162 return None;
163 };
164 let unbounded = match &only.path {
165 AccessPath::TableScan { .. } => true,
166 AccessPath::IndexSeek {
167 equalities,
168 low,
169 high,
170 ..
171 } => equalities.is_empty() && low.is_none() && high.is_none(),
172 _ => false,
173 };
174 if !extreme || !unbounded || !select.group_by.is_empty() || aggregate.distinct {
175 return None;
176 }
177 let [argument] = aggregate.arguments.as_slice() else {
178 return None;
179 };
180 let bound = select.sources.first()?;
181 let name = loop_name(bound);
182 let column = match argument {
183 BoundExpr::Column { source, column, .. } if *source == only.id => *column,
184 _ => return Some(format!("SEARCH {name}")),
185 };
186 if only.table.rowid_alias == Some(column) {
187 return Some(format!("SEARCH {name}"));
188 }
189 let Some(index) = only.table.indexes.iter().find(|index| {
190 index
191 .columns
192 .first()
193 .is_some_and(|key| key.column == Some(column))
194 && index.partial_sql.is_none()
195 && index.metric.is_none()
196 }) else {
197 return Some(format!("SEARCH {name}"));
198 };
199 let reads = select.columns_read(only.id);
200 let covering = !reads.opaque
201 && reads.columns.iter().all(|read| {
202 Some(*read) == only.table.rowid_alias
203 || index.columns.iter().any(|key| key.column == Some(*read))
204 });
205 let kind = if covering { "COVERING INDEX" } else { "INDEX" };
206 Some(format!(
207 "SEARCH {name} USING {kind} {}",
208 String::from_utf8_lossy(&index.name)
209 ))
210}
211
212fn node_name(source: &BoundSource) -> String {
220 if (source.derived.cte || source.derived.view) && !source.derived.name.is_empty() {
221 return String::from_utf8_lossy(&source.derived.name).into_owned();
222 }
223 loop_name(source)
224}
225
226pub(super) fn loop_name(source: &BoundSource) -> String {
231 if let Some(rows) = values_rows(source) {
232 return format!("{rows}-ROW VALUES CLAUSE");
233 }
234 if source.derived.anonymous {
235 if let SourceRows::Subquery(block) = &source.rows {
236 return format!("(subquery-{})", last_serial(block));
237 }
238 }
239 String::from_utf8_lossy(&source.alias).into_owned()
240}
241
242fn values_rows(source: &BoundSource) -> Option<usize> {
249 match &source.rows {
250 SourceRows::Subquery(block) if block.values.len() > 1 => Some(block.values.len()),
251 _ => None,
252 }
253}
254
255pub(super) fn last_serial(block: &BoundSelect) -> u32 {
260 block
261 .compounds
262 .last()
263 .map_or(block.serial, |(_, arm)| arm.serial)
264}
265
266pub(super) fn runs_as_coroutine(select: &BoundSelect, position: usize) -> bool {
278 let Some(source) = select.sources.get(position) else {
279 return false;
280 };
281 let note = &source.derived;
282 if note.cte
283 && (note.materialized == Some(true) || (note.uses >= 2 && note.materialized != Some(false)))
284 {
285 return false;
286 }
287 if select
288 .sources
289 .first()
290 .is_some_and(|first| matches!(first.join, JoinKind::Right | JoinKind::Full))
291 {
292 return false;
293 }
294 if position == 0 {
295 return true;
296 }
297 let mut at = position;
298 loop {
299 let Some(held) = select.sources.get(at) else {
300 return false;
301 };
302 if matches!(
303 held.join,
304 JoinKind::Left | JoinKind::Full | JoinKind::Right | JoinKind::Cross
305 ) {
306 return false;
307 }
308 if at == 0 {
309 return true;
310 }
311 at -= 1;
312 if select
313 .sources
314 .get(at)
315 .is_some_and(|before| !matches!(before.rows, SourceRows::Table))
316 {
317 return false;
318 }
319 }
320}
321
322fn derived_nodes(plan: &PhysicalPlan, depth: u16, out: &mut Vec<PlanLine>) {
332 let mut filled: Vec<String> = Vec::new();
333 for (position, bound) in plan.select.sources.iter().enumerate() {
334 let Some(planned) = plan.sources.iter().find(|held| held.id == bound.id) else {
335 continue;
336 };
337 if values_rows(bound).is_some() {
338 continue;
339 }
340 let coroutine = runs_as_coroutine(&plan.select, position);
341 let name = node_name(bound);
342 if bound.derived.cte && !coroutine {
343 if filled.contains(&name) {
344 continue;
345 }
346 filled.push(name.clone());
347 }
348 let kind = if coroutine {
349 "CO-ROUTINE"
350 } else {
351 "MATERIALIZE"
352 };
353 match &planned.path {
354 AccessPath::Subquery { plan: inner, .. } => {
355 push(out, depth, format!("{kind} {name}"));
356 tree_of(inner, depth + 1, out);
357 }
358 AccessPath::Recursive { seeds, steps, .. } => {
359 push(out, depth, format!("{kind} {name}"));
360 push(out, depth + 1, "SETUP");
361 for (_, seed) in seeds {
362 tree_of(seed, depth + 2, out);
363 }
364 push(out, depth + 1, "RECURSIVE STEP");
365 for (_, step) in steps {
366 tree_of(step, depth + 2, out);
367 }
368 }
369 _ => {}
370 }
371 }
372}
373
374fn expression_subqueries(plan: &PhysicalPlan, depth: u16, out: &mut Vec<PlanLine>) {
385 let select = &plan.select;
386 let mut roots: Vec<&BoundExpr> = Vec::new();
387 roots.extend(select.columns.iter().map(|column| &column.expr));
388 roots.extend(select.group_by.iter());
389 roots.extend(select.having.iter());
390 roots.extend(select.order_by.iter().map(|term| &term.expr));
391 subquery_nodes_into(select.filter.as_ref(), &roots, plan.levers, depth, out);
392}
393
394pub fn subquery_nodes(
401 filter: Option<&BoundExpr>,
402 others: &[&BoundExpr],
403 levers: Levers,
404) -> Vec<PlanLine> {
405 let mut out = Vec::new();
406 subquery_nodes_into(filter, others, levers, 0, &mut out);
407 out
408}
409
410fn subquery_nodes_into(
423 filter: Option<&BoundExpr>,
424 others: &[&BoundExpr],
425 levers: Levers,
426 depth: u16,
427 out: &mut Vec<PlanLine>,
428) {
429 let mut filtering: Vec<&BoundExpr> = Vec::new();
430 if let Some(filter) = filter {
431 subqueries_in(filter, &mut filtering);
432 }
433 let in_filter: Vec<usize> = filtering
434 .iter()
435 .filter_map(|expr| match expr {
436 BoundExpr::Subquery { id, .. } => Some(*id),
437 _ => None,
438 })
439 .collect();
440 let mut found = filtering;
441 for root in others {
442 subqueries_in(root, &mut found);
443 }
444 let mut seen: Vec<usize> = Vec::new();
445 for expr in found {
446 let BoundExpr::Subquery {
447 id, kind, block, ..
448 } = expr
449 else {
450 continue;
451 };
452 if seen.contains(id) {
453 continue;
454 }
455 seen.push(*id);
456 let correlated = if block.correlations.is_empty() {
457 ""
458 } else {
459 "CORRELATED "
460 };
461 let noun = match kind {
462 SubqueryKind::In => "LIST",
463 _ => "SCALAR",
464 };
465 push(
466 out,
467 depth,
468 format!("{correlated}{noun} SUBQUERY {}", last_serial(block)),
469 );
470 let inner = plan_select_with((**block).clone(), levers);
471 tree_of(&inner, depth + 1, out);
472 if *kind == SubqueryKind::In && in_filter.contains(id) {
473 push(out, depth + 1, "CREATE BLOOM FILTER");
474 }
475 }
476}
477
478fn subqueries_in<'e>(expr: &'e BoundExpr, found: &mut Vec<&'e BoundExpr>) {
483 if matches!(expr, BoundExpr::Subquery { .. }) {
484 found.push(expr);
485 if let BoundExpr::Subquery {
486 operand: Some(operand),
487 ..
488 } = expr
489 {
490 subqueries_in(operand, found);
491 }
492 return;
493 }
494 for child in expr.children() {
495 subqueries_in(child, found);
496 }
497}
498
499fn operator_name(op: CompoundOp) -> &'static str {
503 match op {
504 CompoundOp::Union => "UNION",
505 CompoundOp::UnionAll => "UNION ALL",
506 CompoundOp::Intersect => "INTERSECT",
507 CompoundOp::Except => "EXCEPT",
508 }
509}
510
511fn first_arm(plan: &PhysicalPlan) -> PhysicalPlan {
516 let mut arm = plan.clone();
517 arm.compounds.clear();
518 arm.needs_sort = false;
519 arm.reverse = false;
520 arm.select.order_by.clear();
521 arm.select.limit = None;
522 arm.select.offset = None;
523 arm
524}
525
526fn compound_tree(plan: &PhysicalPlan, depth: u16, out: &mut Vec<PlanLine>) {
537 let ordered = !plan.select.order_by.is_empty();
538 if !ordered
539 && plan
540 .compounds
541 .iter()
542 .all(|(op, _)| *op == CompoundOp::UnionAll)
543 {
544 push(out, depth, "COMPOUND QUERY");
545 push(out, depth + 1, "LEFT-MOST SUBQUERY");
546 tree_of(&first_arm(plan), depth + 2, out);
547 for (op, arm) in &plan.compounds {
548 push(out, depth + 1, operator_name(*op));
549 tree_of(arm, depth + 2, out);
550 }
551 return;
552 }
553 let mut arms: Vec<(Option<CompoundOp>, BoundSelect)> = vec![(None, first_arm(plan).select)];
554 for (op, arm) in &plan.compounds {
555 arms.push((Some(*op), arm.select.clone()));
556 }
557 merge_tree(&arms, &plan.select.order_by, plan.levers, depth, out);
558}
559
560fn merge_tree(
573 arms: &[(Option<CompoundOp>, BoundSelect)],
574 order: &[BoundOrderTerm],
575 levers: Levers,
576 depth: u16,
577 out: &mut Vec<PlanLine>,
578) {
579 let Some(((op, last), before)) = arms.split_last() else {
580 return;
581 };
582 let op = op.unwrap_or(CompoundOp::UnionAll);
583 push(out, depth, format!("MERGE ({})", operator_name(op)));
584 push(out, depth + 1, "LEFT");
585 match before {
586 [(_, only)] => sorted_arm(only, order, levers, depth + 2, out),
587 _ => merge_tree(before, order, levers, depth + 2, out),
588 }
589 push(out, depth + 1, "RIGHT");
590 sorted_arm(last, order, levers, depth + 2, out);
591}
592
593fn sorted_arm(
602 arm: &BoundSelect,
603 order: &[BoundOrderTerm],
604 levers: Levers,
605 depth: u16,
606 out: &mut Vec<PlanLine>,
607) {
608 let mut sorted = arm.clone();
609 let mut terms: Vec<BoundOrderTerm> = Vec::new();
610 let mut named: Vec<usize> = Vec::new();
611 for term in order {
612 let BoundExpr::SorterColumn { column } = term.expr else {
613 continue;
614 };
615 let Some(result) = arm.columns.get(usize::from(column)) else {
616 continue;
617 };
618 named.push(usize::from(column));
619 terms.push(BoundOrderTerm {
620 expr: result.expr.clone(),
621 ..term.clone()
622 });
623 }
624 for (position, result) in arm.columns.iter().enumerate() {
625 if named.contains(&position) {
626 continue;
627 }
628 terms.push(BoundOrderTerm {
629 expr: result.expr.clone(),
630 order: crate::ast::SortOrder::Ascending,
631 nulls: crate::ast::NullOrder::First,
632 collation: Collation::Binary,
633 });
634 }
635 sorted.order_by = terms;
636 let planned = plan_select_with(sorted, levers);
637 select_tree(&planned, depth, out);
638}
639
640fn window_tree(plan: &PhysicalPlan, depth: u16, out: &mut Vec<PlanLine>) {
650 let number = highest_serial(&plan.select).saturating_add(1);
651 let name = format!("(subquery-{number})");
652 push(out, depth, format!("CO-ROUTINE {name}"));
653 derived_nodes(plan, depth + 1, out);
654 for line in describe::loop_lines(plan) {
655 push(out, depth + 1, line);
656 }
657 let sorts = plan
658 .select
659 .windows
660 .first()
661 .is_some_and(|window| !window.partition_by.is_empty() || !window.order_by.is_empty());
662 if sorts {
663 push(out, depth + 1, "USE TEMP B-TREE FOR ORDER BY");
664 }
665 push(out, depth, format!("SCAN {name}"));
666 let same_order = plan.select.windows.first().is_some_and(|window| {
667 window.partition_by.is_empty()
668 && window.order_by.len() == plan.select.order_by.len()
669 && window
670 .order_by
671 .iter()
672 .zip(&plan.select.order_by)
673 .all(|(left, right)| left.expr == right.expr && left.order == right.order)
674 });
675 if !plan.select.order_by.is_empty() && !same_order {
676 push(out, depth, "USE TEMP B-TREE FOR ORDER BY");
677 }
678}
679
680fn highest_serial(select: &BoundSelect) -> u32 {
684 let mut highest = select.serial;
685 for source in &select.sources {
686 if let SourceRows::Subquery(block) = &source.rows {
687 highest = highest.max(highest_serial(block));
688 }
689 }
690 for (_, arm) in &select.compounds {
691 highest = highest.max(highest_serial(arm));
692 }
693 let mut probe = select.clone();
694 crate::rewrite::rewrite_select(&mut probe, &mut |expr: &mut BoundExpr| {
695 if let Some(block) = expr.block_mut() {
696 highest = highest.max(block.serial);
697 }
698 });
699 highest
700}