Skip to main content

inillucent_sql/plan/
pushdown.rs

1//! Pushing a `WHERE` condition on a derived table's columns into the derived
2//! table.
3//!
4//! Invariant: **a condition is copied into a derived table only when doing so
5//! cannot change which rows the statement returns, and the original stays where
6//! it was.** The copy is a filter the inner query applies before it builds its
7//! rows, so the inner planner can seek by key instead of building every row of
8//! a view. The outer condition is still tested on every row the derived table
9//! produces, so if a rule here were too generous the cost would be a condition
10//! tested twice, never a row that should have been filtered.
11//!
12//! This is SQLite's push-down optimisation, restricted to the cases where it is
13//! plainly sound. Measured on the coffee shop example's database before it:
14//! `SELECT * FROM order_summary WHERE id = 57` over a view of three joins and a
15//! correlated subquery took 2.50 ms against 0.048 ms for the same query written
16//! without the view, because every row of the view, correlated subquery
17//! included, was built before the `WHERE` was applied. SQLite plans both as a
18//! primary key search.
19//!
20//! Here rather than in [`super`] because `plan.rs` is at its recorded size.
21
22use super::*;
23
24mod unused;
25
26/// Copies each `WHERE` conjunct that reads only one derived table's columns
27/// into that derived table's own `WHERE`, and then replaces each derived
28/// table's unread result columns with NULL.
29///
30/// The second step is in [`unused`]. It runs here because it is the other
31/// half of treating a derived table the way SQLite's flattener does, and
32/// `plan.rs` is at its recorded size.
33///
34/// @param select - the statement being planned, whose derived tables may gain
35///   a filter and lose result columns
36pub(super) fn push_into_derived_tables(select: &mut BoundSelect) {
37    push_filters(select);
38    unused::drop_unread_columns(select);
39}
40
41/// Copies each `WHERE` conjunct that reads only one derived table's columns
42/// into that derived table's own `WHERE`.
43///
44/// @param select - the statement being planned, whose derived tables may gain
45///   a filter
46fn push_filters(select: &mut BoundSelect) {
47    let Some(filter) = select.filter.as_ref() else {
48        return;
49    };
50    // Nothing is split or copied for a statement with no derived table, which
51    // is almost every statement: the compile of `SELECT id FROM t WHERE email
52    // = ?1` has an allocation budget, and splitting its `WHERE` here took three
53    // of them to find nothing to push.
54    if !select
55        .sources
56        .iter()
57        .any(|source| matches!(source.rows, SourceRows::Subquery(_)))
58    {
59        return;
60    }
61    // A `RIGHT` or `FULL` join can null extend any term before it, so a
62    // statement with one pushes nothing.
63    if select
64        .sources
65        .iter()
66        .any(|source| matches!(source.join, JoinKind::Right | JoinKind::Full))
67    {
68        return;
69    }
70    let conjuncts = conjunction(filter);
71    for source in &mut select.sources {
72        // The right side of a `LEFT JOIN` is null extended when nothing in it
73        // matches, and a condition such as `v.x IS NULL` is true of a null
74        // extended row and false of the rows the push would have removed.
75        if source.join == JoinKind::Left {
76            continue;
77        }
78        let id = source.id;
79        let SourceRows::Subquery(block) = &mut source.rows else {
80            continue;
81        };
82        // The references of a shared common table expression read one set of
83        // rows, so a filter pushed into one of them would change the others.
84        if block.shared.is_some() {
85            continue;
86        }
87        if block.compounds.is_empty() {
88            if accepts_a_pushed_filter(block) {
89                push_into_arm(block, id, &conjuncts);
90            }
91        } else if accepts_a_compound_filter(block) {
92            push_into_compound(block, id, &conjuncts);
93        }
94    }
95}
96
97/// Copies each conjunct on the derived table into one `SELECT` that produces
98/// the derived table's rows, with the conjunct's columns replaced by the
99/// expressions that compute them in that `SELECT`.
100///
101/// @param arm - the `SELECT` (one arm of a compound, or the whole derived table)
102/// @param id - the derived table's statement-wide number
103/// @param conjuncts - the terms of the outer `WHERE`
104fn push_into_arm(arm: &mut BoundSelect, id: usize, conjuncts: &[BoundExpr]) {
105    for conjunct in conjuncts {
106        let mut used = Vec::new();
107        conjunct.sources_used(&mut used);
108        if used.as_slice() != [id] || !pushable(conjunct, id) {
109            continue;
110        }
111        let Some(inner) = substituted(conjunct, id, arm) else {
112            continue;
113        };
114        arm.filter = Some(match arm.filter.take() {
115            Some(existing) => BoundExpr::And(Box::new(existing), Box::new(inner)),
116            None => inner,
117        });
118    }
119}
120
121/// Copies the conjuncts into every arm of a compound derived table.
122///
123/// **Each arm compares with its own columns' affinity and collation.** SQLite
124/// pushes the term into every arm, and a term `x > 7` over `SELECT x FROM t
125/// UNION ALL SELECT y FROM v` with a TEXT `x` and an INTEGER `y` compares text
126/// with text in the first arm and integers in the second. Tested after the
127/// compound, the column has no affinity and a text `'1'` is greater than the
128/// integer 7 by storage class. An arm that cannot take a filter (a `VALUES`
129/// list, an aggregate arm) is left as it is; the outer term stays in place.
130///
131/// @param block - the compound derived table; its first arm is the block itself
132/// @param id - the derived table's statement-wide number
133/// @param conjuncts - the terms of the outer `WHERE`
134fn push_into_compound(block: &mut BoundSelect, id: usize, conjuncts: &[BoundExpr]) {
135    if accepts_an_arm_filter(block) {
136        push_into_arm(block, id, conjuncts);
137    }
138    for (_, arm) in &mut block.compounds {
139        if accepts_an_arm_filter(arm) && arm.compounds.is_empty() {
140            push_into_arm(arm, id, conjuncts);
141        }
142    }
143}
144
145/// Reports whether a filter on a compound's result may be copied into its arms.
146///
147/// SQLite refuses a compound with a `LIMIT` or `OFFSET`, which count rows
148/// before the filter, and a compound that has a window function in any arm.
149/// When an arm is joined by `UNION`, `INTERSECT` or `EXCEPT` it also refuses
150/// when the compound's `ORDER BY` has a term that is not a result column.
151///
152/// @param block - the compound derived table
153fn accepts_a_compound_filter(block: &BoundSelect) -> bool {
154    let all_union_all = block
155        .compounds
156        .iter()
157        .all(|(op, _)| *op == crate::ast::CompoundOp::UnionAll);
158    block.limit.is_none()
159        && block.offset.is_none()
160        && (all_union_all || block.order_by.is_empty())
161        && block.windows.is_empty()
162        && block
163            .compounds
164            .iter()
165            .all(|(_, arm)| arm.windows.is_empty())
166}
167
168/// Reports whether one arm of a compound can take a copied filter.
169///
170/// An arm with its own `DISTINCT`, grouping, aggregate or window computes its
171/// result columns over rows a filter would remove, and a `VALUES` list has no
172/// expressions to substitute.
173///
174/// @param arm - one arm of the compound
175fn accepts_an_arm_filter(arm: &BoundSelect) -> bool {
176    !arm.distinct
177        && arm.group_by.is_empty()
178        && arm.aggregates.is_empty()
179        && arm.having.is_none()
180        && arm.windows.is_empty()
181        && arm.values.is_empty()
182}
183
184/// Reports whether a derived table's rows are the same whether a condition on
185/// its result columns is applied before it builds them or after.
186///
187/// **Not for a `LIMIT` or an `OFFSET`**, which count rows before the condition;
188/// **not for `DISTINCT`**, which keeps one row of several equal under its own
189/// collation, so a condition under a different collation can keep a different
190/// one; **not for grouping or a window**, whose result columns are computed
191/// over rows the condition would remove; and **not for a compound**, which is
192/// several blocks.
193///
194/// @param block - the derived table's query
195fn accepts_a_pushed_filter(block: &BoundSelect) -> bool {
196    block.compounds.is_empty()
197        && block.limit.is_none()
198        && block.offset.is_none()
199        && !block.distinct
200        && block.group_by.is_empty()
201        && block.aggregates.is_empty()
202        && block.having.is_none()
203        && block.windows.is_empty()
204        && block.values.is_empty()
205}
206
207/// Reports whether a condition is one that may be evaluated anywhere, any
208/// number of times, with the same answer.
209///
210/// A whitelist: a subquery, an aggregate, a window value, a registered function
211/// whose determinism the planner cannot see, and the scalar functions whose
212/// answer changes from call to call are all refused.
213///
214/// @param expr - the condition, or a part of it
215/// @param id - the derived table's statement-wide number; its rowid has no
216///   inner expression to stand for it
217fn pushable(expr: &BoundExpr, id: usize) -> bool {
218    let this = match expr {
219        BoundExpr::Rowid { source } => *source != id,
220        BoundExpr::Subquery { .. }
221        | BoundExpr::Aggregate { .. }
222        | BoundExpr::WindowRef { .. }
223        | BoundExpr::SorterColumn { .. }
224        | BoundExpr::External { .. }
225        | BoundExpr::VirtualFunction { .. }
226        | BoundExpr::Raise { .. } => false,
227        BoundExpr::Function { func, .. } => !matches!(
228            func,
229            crate::function::ScalarFunc::Random
230                | crate::function::ScalarFunc::RandomBlob
231                | crate::function::ScalarFunc::Changes
232                | crate::function::ScalarFunc::TotalChanges
233                | crate::function::ScalarFunc::LastInsertRowid
234        ),
235        _ => true,
236    };
237    this && expr.children().iter().all(|child| pushable(child, id))
238}
239
240/// Reports whether a condition may be tested more than once with the same
241/// answer each time.
242///
243/// A condition on an outer term can be tested before a lateral join runs its
244/// function, so that the function is not called for rows the condition
245/// removes. It is tested again with the rest of the `WHERE`, which is only
246/// harmless for a condition with no subquery, no random function and no
247/// registered function whose determinism the planner cannot see.
248///
249/// @param expr - the condition
250pub fn is_repeatable_condition(expr: &BoundExpr) -> bool {
251    pushable(expr, usize::MAX)
252}
253
254/// Reports whether an expression calls a function whose answer changes from one
255/// call to the next, such as `random()`.
256///
257/// @param expr - the expression, or a part of it
258pub fn calls_a_volatile_function(expr: &BoundExpr) -> bool {
259    let this = matches!(
260        expr,
261        BoundExpr::Function {
262            func: crate::function::ScalarFunc::Random
263                | crate::function::ScalarFunc::RandomBlob
264                | crate::function::ScalarFunc::Changes
265                | crate::function::ScalarFunc::TotalChanges
266                | crate::function::ScalarFunc::LastInsertRowid,
267            ..
268        }
269    );
270    this || expr
271        .children()
272        .iter()
273        .any(|child| calls_a_volatile_function(child))
274}
275
276/// Returns a condition with each of the derived table's columns replaced by
277/// the expression that computes it inside the derived table.
278///
279/// `None` when a column's expression is one that should not be evaluated in a
280/// `WHERE`, such as a correlated subquery: the condition is then left outside,
281/// where it was.
282///
283/// @param conjunct - the condition, over the derived table's columns
284/// @param id - the derived table's statement-wide number
285/// @param block - the derived table's query
286fn substituted(conjunct: &BoundExpr, id: usize, block: &BoundSelect) -> Option<BoundExpr> {
287    let mut copy = conjunct.clone();
288    replace_columns(&mut copy, id, block).then_some(copy)
289}
290
291/// Replaces the derived table's columns in place, reporting whether every one
292/// could be replaced.
293///
294/// @param expr - the expression being rewritten
295/// @param id - the derived table's statement-wide number
296/// @param block - the derived table's query
297fn replace_columns(expr: &mut BoundExpr, id: usize, block: &BoundSelect) -> bool {
298    if let BoundExpr::Column { source, column, .. } = expr {
299        if *source != id {
300            return true;
301        }
302        let Some(inner) = block.columns.get(usize::from(*column)) else {
303            return false;
304        };
305        if !pushable(&inner.expr, usize::MAX) {
306            return false;
307        }
308        *expr = inner.expr.clone();
309        return true;
310    }
311    let replaced = expr
312        .children_mut()
313        .into_iter()
314        .all(|child| replace_columns(child, id, block));
315    if replaced {
316        refresh_comparison_rules(expr);
317    }
318    replaced
319}
320
321/// Recomputes the affinity and collation a comparison applies, from its
322/// operands as they are now.
323///
324/// A comparison fixes both when the statement is bound, from the operands it
325/// was written with. After a derived table's column is replaced by the
326/// expression of one arm, the operand may have a different affinity: the
327/// derived table's column of a compound has none when the arms disagree, and
328/// the arm's own column has its declared one. SQLite compares the substituted
329/// expression, so the comparison is rebuilt from it.
330///
331/// @param expr - the expression whose children were just substituted
332fn refresh_comparison_rules(expr: &mut BoundExpr) {
333    use crate::bind::comparison_rules;
334    match expr {
335        BoundExpr::Compare {
336            left,
337            right,
338            affinity,
339            collation,
340            ..
341        }
342        | BoundExpr::Is {
343            left,
344            right,
345            affinity,
346            collation,
347            ..
348        } => (*affinity, *collation) = comparison_rules(left, right),
349        BoundExpr::Between {
350            operand,
351            low,
352            high,
353            low_affinity,
354            low_collation,
355            high_affinity,
356            high_collation,
357            ..
358        } => {
359            (*low_affinity, *low_collation) = comparison_rules(operand, low);
360            (*high_affinity, *high_collation) = comparison_rules(operand, high);
361        }
362        BoundExpr::InList {
363            operand,
364            list,
365            affinity,
366            collation,
367            ..
368        } => {
369            if let Some(first) = list.first() {
370                (*affinity, *collation) = comparison_rules(operand, first);
371            }
372        }
373        BoundExpr::Case {
374            operand: Some(operand),
375            branches,
376            comparisons,
377            ..
378        } => {
379            *comparisons = branches
380                .iter()
381                .map(|(when, _)| comparison_rules(operand, when))
382                .collect();
383        }
384        _ => {}
385    }
386}