Skip to main content

inillucent_sql/plan/
tree.rs

1//! The tree `EXPLAIN QUERY PLAN` prints.
2//!
3//! Invariant: **each line sits under the line SQLite 3.53.4 puts it under, and
4//! the nodes are SQLite's.** A derived table SQLite runs as a co-routine is a
5//! `CO-ROUTINE` node holding its own plan, one it fills once is a
6//! `MATERIALIZE` node, a `UNION ALL` is a `COMPOUND QUERY` with one child per
7//! arm, the other compound operators are a `MERGE` of a `LEFT` and a `RIGHT`
8//! side each sorted on the result columns, and an expression subquery is a
9//! `SCALAR SUBQUERY` or `LIST SUBQUERY` node numbered the way SQLite numbers
10//! its `Select`s. The lines inside a node are the ones `describe` writes.
11//!
12//! A shell draws the tree from each line's parent. The flat list this replaced
13//! drew every line at the top level, so the three corpus cases with a derived
14//! table differed from SQLite in their plan text while their rows matched.
15
16use super::describe;
17use super::*;
18use crate::bind::{BoundOrderTerm, SubqueryKind};
19
20/// One line of `EXPLAIN QUERY PLAN`, and how deep in the tree it sits.
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct PlanLine {
23    /// How many nodes are above it; zero is a line at the top level.
24    pub depth: u16,
25    /// The text, such as `SCAN t` or `CO-ROUTINE x`.
26    pub detail: String,
27}
28
29/// Appends one line.
30///
31/// @param out - the lines so far
32/// @param depth - its depth
33/// @param detail - its text
34fn push(out: &mut Vec<PlanLine>, depth: u16, detail: impl Into<String>) {
35    out.push(PlanLine {
36        depth,
37        detail: detail.into(),
38    });
39}
40
41/// Appends the lines of one plan at a depth.
42///
43/// @param plan - the plan
44/// @param depth - the depth of its top level lines
45/// @param out - the lines so far
46pub(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
58/// Appends the lines of one plan that is not a compound: its derived tables,
59/// its loops, its expression subqueries and its sorters, in that order.
60///
61/// @param plan - the plan
62/// @param depth - the depth of its top level lines
63/// @param out - the lines so far
64fn 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
91/// Returns the plan of a `UNION ALL` derived table SQLite flattens into the
92/// statement, which then has no node of its own.
93///
94/// SQLite's compound flattening (restriction 17) turns `SELECT * FROM (SELECT
95/// .. UNION ALL SELECT ..) WHERE a = 7` into a compound of the arms, each with
96/// the `WHERE` in it. This engine fills the derived table instead, with the
97/// `WHERE` pushed into each arm, so the arms' plans are the ones SQLite prints.
98/// It is done when the derived table is the only FROM term, every operator is
99/// `UNION ALL`, no arm aggregates, is `DISTINCT` or has a window, the derived
100/// table has no `ORDER BY` or `LIMIT`, and the statement neither aggregates,
101/// sorts, limits, is `DISTINCT` nor has a window.
102///
103/// @param plan - the statement's plan
104fn 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
143/// Returns the loop line SQLite prints for a statement that is one `min()` or
144/// one `max()` over one table, which it answers by seeking the first or last
145/// entry rather than scanning.
146///
147/// The line is `SEARCH t USING COVERING INDEX i` when an index starts with the
148/// argument column and holds every column the statement reads, `SEARCH t USING
149/// INDEX i` when it starts with it and does not, and `SEARCH t` otherwise.
150///
151/// @param plan - the statement's plan
152fn 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
212/// Returns the name SQLite gives a FROM term in a node: the name of the CTE or
213/// view it reads, else its alias, else `(subquery-N)`.
214///
215/// This is SQLite's `%!S`, which prefers the name to the alias. A loop line
216/// uses `%S`, which prefers the alias; see [`loop_name`].
217///
218/// @param source - the bound FROM term
219fn 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
226/// Returns the name SQLite gives a FROM term in a loop line: its alias, else
227/// `(subquery-N)` for a derived table written with no alias.
228///
229/// @param source - the bound FROM term
230pub(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
242/// Returns how many rows a derived table of several `VALUES` rows has.
243///
244/// SQLite reads such a table straight from its rows, with no node of its own,
245/// and names the loop `SCAN 2-ROW VALUES CLAUSE`.
246///
247/// @param source - the bound FROM term
248fn 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
255/// Returns SQLite's number for a block: its last arm's, because SQLite's
256/// `Select` for a compound is the rightmost one.
257///
258/// @param block - the block
259pub(super) fn last_serial(block: &BoundSelect) -> u32 {
260    block
261        .compounds
262        .last()
263        .map_or(block.serial, |(_, arm)| arm.serial)
264}
265
266/// Reports whether SQLite runs the derived table at one FROM position as a
267/// co-routine rather than filling a table with it, as its
268/// `fromClauseTermCanBeCoroutine` decides.
269///
270/// A `MATERIALIZED` CTE, and a CTE used more than once that is not `NOT
271/// MATERIALIZED`, are filled. Otherwise the first term is a co-routine, and a
272/// later one is when neither it nor any term before it is joined by `LEFT` or
273/// `CROSS` and no term before it is a derived table.
274///
275/// @param select - the statement
276/// @param position - the FROM position
277pub(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
322/// Appends a `CO-ROUTINE` or `MATERIALIZE` node for each derived table, in
323/// FROM order, each holding the derived table's own plan.
324///
325/// A CTE that is filled once for several references is named once, at its
326/// first reference.
327///
328/// @param plan - the plan
329/// @param depth - the depth of the nodes
330/// @param out - the lines so far
331fn 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
374/// Appends a node for each subquery the statement uses as a value, after its
375/// loops: the `WHERE`'s first, then the result columns', then the rest.
376///
377/// `LIST SUBQUERY` for an `IN`, `SCALAR SUBQUERY` for the others, prefixed
378/// `CORRELATED` when the subquery reads the statement. Each holds the
379/// subquery's own plan.
380///
381/// @param plan - the plan
382/// @param depth - the depth of the nodes
383/// @param out - the lines so far
384fn 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
394/// Returns the nodes for the subqueries a write uses as values: those in its
395/// `WHERE`, then those in its other expressions, at the top level.
396///
397/// @param filter - the write's `WHERE`
398/// @param others - its other expressions, such as an `UPDATE`'s new values
399/// @param levers - which optimizations are on
400pub 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
410/// Appends a node for each subquery in a `WHERE` and then in other expressions.
411///
412/// An `IN` subquery in the `WHERE` tests each row against the subquery's rows,
413/// and SQLite builds a bloom filter over them for that, which its node shows
414/// as a last child. SQLite seeks an index by an `IN` subquery where it can and
415/// builds no filter then; this engine does not seek by one.
416///
417/// @param filter - the `WHERE`
418/// @param others - the other expressions
419/// @param levers - which optimizations are on
420/// @param depth - the depth of the nodes
421/// @param out - the lines so far
422fn 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
478/// Collects the subqueries an expression holds, without looking inside them.
479///
480/// @param expr - the expression
481/// @param found - where they are collected
482fn 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
499/// Returns the word `EXPLAIN QUERY PLAN` names a compound operator by.
500///
501/// @param op - the operator
502fn 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
511/// Returns the plan of a compound's first arm on its own, without the
512/// compound's ordering, as `run_arm` runs it.
513///
514/// @param plan - the compound's plan
515fn 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
526/// Appends a compound's lines.
527///
528/// A `UNION ALL` with no `ORDER BY` runs its arms one after another under a
529/// `COMPOUND QUERY` node. Anything else is merged: SQLite sorts each side on
530/// the result columns and walks the two sorted lists together, which
531/// `merge_tree` describes.
532///
533/// @param plan - the compound's plan, whose first arm is the plan itself
534/// @param depth - the depth of the top node
535/// @param out - the lines so far
536fn 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
560/// Appends the `MERGE` of a compound's arms.
561///
562/// The last arm is the right side, and everything before it is the left side,
563/// itself a merge when it has more than one arm. Each side is sorted on the
564/// compound's `ORDER BY` followed by the result columns it does not name,
565/// which is the order SQLite's `multiSelectOrderBy` merges in.
566///
567/// @param arms - the arms, each with the operator joining it to the ones before
568/// @param order - the compound's `ORDER BY`
569/// @param levers - which optimizations are on
570/// @param depth - the depth of the `MERGE` node
571/// @param out - the lines so far
572fn 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
593/// Appends the lines of one arm of a merge, planned as if it had the merge's
594/// ordering, so a sorter appears when its loops do not give that order.
595///
596/// @param arm - the arm
597/// @param order - the compound's `ORDER BY`, by result column
598/// @param levers - which optimizations are on
599/// @param depth - the depth of its lines
600/// @param out - the lines so far
601fn 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
640/// Appends the lines of a statement with a window function.
641///
642/// SQLite rewrites it into a co-routine over the statement's rows, sorted into
643/// the window's order, and scans that. The co-routine is numbered after every
644/// `Select` the parser wrote.
645///
646/// @param plan - the plan
647/// @param depth - the depth of the top lines
648/// @param out - the lines so far
649fn 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
680/// Returns the largest SQLite number of any block in a statement.
681///
682/// @param select - the statement
683fn 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}