Skip to main content

rudb_opt/
empty.rs

1//! Replacing a subtree that cannot produce a row with a relation that produces none.
2//!
3//! `WHERE false` is the query somebody writes to ask for nothing, and a plan that still has a scan
4//! under it answers nothing after reading the whole table. On ClickBench that is a hundred million
5//! rows off disk to produce an empty result, so this is a speed matter, but it is a correctness one
6//! first: `spec/09-optimizer.md` asks that a query with an unsatisfiable predicate not touch the
7//! storage it was written against, and a scan that runs is a scan that can report an error about a
8//! file the query was never going to read from.
9//!
10//! # What counts as empty
11//!
12//! A filter whose predicate is a false or a null constant, since `WHERE` keeps the rows where the
13//! predicate is true and neither of those ever is. A limit of zero rows, and the top N it fuses
14//! into. A `VALUES` with no rows. Anything above one of those that passes its input through, which
15//! is a filter, a sort, a limit, a top N, a `DISTINCT` and a projection.
16//!
17//! The pullup stops at a group by, which is the operator this pass exists to be careful about. An
18//! ungrouped aggregate over no rows produces one row and not none, so `SELECT count(*) FROM t WHERE
19//! false` is `0` rather than an empty answer, and a pass that treated the aggregate as empty because
20//! its input was would return the wrong number of rows. The empty relation is put under the
21//! aggregate and the aggregate stays.
22//!
23//! It also stops at a join, a cross product and a set operation, for a reason that is about spelling
24//! rather than about semantics. An empty relation here is a `Node::Values`, which binds its columns
25//! to one table index, and those three produce columns bound to two of them or to an index of their
26//! own that is not either side's. Replacing one would mean rewriting every binding above it, so an
27//! empty side of a join is left as an empty side of a join, which the executor already handles by
28//! finding no rows to pair with.
29//!
30//! # Why it is a pullup rather than a pushdown
31//!
32//! The walk is from the root, and the highest node that cannot produce a row is the one replaced, so
33//! everything beneath it goes away in one step rather than a level per run. That is also what makes
34//! the pass settle: a plan it has run over has an empty `VALUES` where the empty subtree was, and an
35//! empty `VALUES` is the answer this pass would give for it again.
36//!
37//! # Where the always true predicate went
38//!
39//! The other half of constant pruning, `WHERE true`, is in `crate::filter`. A conjunct that is a
40//! true constant is dropped as the pass puts the filter back together, and a filter with nothing
41//! left in it is not rebuilt, which is where the binary does it too.
42
43use rudb_common::{Field, Result};
44use rudb_plan::{Expr, ExprRef, Node, NodeRef, Plan, Slice};
45
46use crate::pass::{Context, Pass};
47
48/// Replaces a subtree that cannot produce a row with an empty relation.
49#[derive(Debug, Clone, Copy)]
50pub struct EmptyResultPullup;
51
52impl Pass for EmptyResultPullup {
53    fn name(&self) -> &'static str {
54        "empty_result_pullup"
55    }
56
57    fn run(&self, plan: &mut Plan, _context: &Context) -> Result<()> {
58        prune(plan);
59        Ok(())
60    }
61}
62
63/// Replaces every highest empty subtree in `plan` with an empty relation.
64pub fn prune(plan: &mut Plan) {
65    let root = plan.root();
66    walk(plan, root);
67}
68
69/// Replaces `at` if it is empty, and otherwise looks under it.
70///
71/// Writing the replacement over the node's own slot rather than appending one is what keeps whatever
72/// pointed at it pointing at the right thing. It is allowed because an empty relation is a leaf, and
73/// the arena only asks that a node's children sit behind it, which a node with no children does
74/// however early its slot is.
75fn walk(plan: &mut Plan, at: NodeRef) {
76    if !already_empty(plan, at) && empty(plan, at) {
77        if let Some((index, columns)) = columns_of(plan, at) {
78            let rows = plan.add_rows(&[]);
79            *plan.node_mut(at) = Node::Values { index, columns, rows };
80            return;
81        }
82    }
83    for child in plan.node(at).children().into_iter().flatten() {
84        walk(plan, child);
85    }
86}
87
88/// Whether this node is already the empty relation, which is what the pass leaves behind.
89///
90/// Without this the second run would rewrite the `VALUES` it wrote on the first one into another
91/// `VALUES` that prints the same, which costs a slot per run and, more to the point, is a pass that
92/// keeps finding work on a plan it has already finished with.
93fn already_empty(plan: &Plan, at: NodeRef) -> bool {
94    match *plan.node(at) {
95        Node::Values { rows, .. } => plan.row_list(rows).is_empty(),
96        _ => false,
97    }
98}
99
100/// Whether this node can produce a row.
101///
102/// Only the operators that pass their input through recurse. Everything else answers false, which
103/// for a group by is the rule and not a missing case.
104fn empty(plan: &Plan, at: NodeRef) -> bool {
105    match *plan.node(at) {
106        Node::Values { rows, .. } => plan.row_list(rows).is_empty(),
107        Node::Filter { input, predicate } => never(plan, predicate) || empty(plan, input),
108        Node::Limit { input, count, .. } => count == Some(0) || empty(plan, input),
109        Node::TopN { input, count, .. } => count == 0 || empty(plan, input),
110        Node::Sort { input, .. } | Node::Distinct { input, .. } | Node::Project { input, .. } => {
111            empty(plan, input)
112        }
113        _ => false,
114    }
115}
116
117/// Whether a predicate keeps no row at all.
118///
119/// A null predicate keeps nothing, the same as a false one. That is `WHERE`'s rule rather than
120/// `=`'s, and it is the difference between a `WHERE` and a `CHECK` constraint.
121fn never(plan: &Plan, predicate: ExprRef) -> bool {
122    let Expr::Constant(value) = *plan.expr(predicate) else {
123        return false;
124    };
125    let value = plan.value(value);
126    value.is_null() || value.as_bool() == Some(false)
127}
128
129/// The table index and the column list an empty relation standing in for this node would need.
130///
131/// `None` where the node's output is not one table index with a field list, which is a join, a cross
132/// product, a set operation and a group by. A group by could be given one by inventing a name per
133/// aggregate, and that would change what `EXPLAIN` prints for a plan the pass had nothing else to do
134/// to, so it is refused here rather than guessed at.
135fn columns_of(plan: &mut Plan, at: NodeRef) -> Option<(u32, Slice)> {
136    match *plan.node(at) {
137        Node::Get { index, columns, .. }
138        | Node::Values { index, columns, .. }
139        | Node::TableFunction { index, columns, .. } => Some((index, columns)),
140        // A projection carries its names, so the fields can be read off the names and the types the
141        // binder already worked out for the expressions.
142        Node::Project { index, exprs, names, .. } => {
143            let exprs = plan.expr_list(exprs).to_vec();
144            let names = plan.name_list(names).to_vec();
145            let fields: Vec<Field> = exprs
146                .iter()
147                .zip(names)
148                .map(|(&expr, name)| Field::new(plan.string(name), plan.expr_type(expr).clone()))
149                .collect();
150            Some((index, plan.add_fields(&fields)))
151        }
152        Node::Filter { input, .. }
153        | Node::Sort { input, .. }
154        | Node::Limit { input, .. }
155        | Node::TopN { input, .. }
156        | Node::Distinct { input, .. } => columns_of(plan, input),
157        _ => None,
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use rudb_plan::Plan;
164
165    use super::prune;
166
167    /// What the plan a text prints looks like once the pass has run over it.
168    fn pruned(text: &str) -> String {
169        let mut plan =
170            Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
171        prune(&mut plan);
172        plan.validate().unwrap_or_else(|error| panic!("{text} did not stay valid: {error}"));
173        plan.to_string()
174    }
175
176    #[test]
177    fn a_false_predicate_takes_the_scan_with_it() {
178        assert_eq!(
179            pruned(concat!(
180                "Filter FALSE::BOOLEAN\n",
181                "  Get memory.main.t AS t #0 [a::INTEGER, b::INTEGER]\n",
182            )),
183            "Values #0 [a::INTEGER, b::INTEGER] rows=[]\n"
184        );
185    }
186
187    /// A null predicate keeps no row either, which is the rule `WHERE` has and `CHECK` does not.
188    #[test]
189    fn a_null_predicate_is_as_empty_as_a_false_one() {
190        assert_eq!(
191            pruned(
192                concat!("Filter NULL::BOOLEAN\n", "  Get memory.main.t AS t #0 [a::INTEGER]\n",)
193            ),
194            "Values #0 [a::INTEGER] rows=[]\n"
195        );
196    }
197
198    #[test]
199    fn a_predicate_that_depends_on_the_row_is_left_alone() {
200        let text = concat!(
201            "Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN\n",
202            "  Get memory.main.t AS t #0 [a::INTEGER]\n",
203        );
204        assert_eq!(pruned(text), text);
205    }
206
207    /// The highest node goes, not the lowest one, so the sort and the projection above the filter
208    /// are gone in one run rather than one of them per run.
209    #[test]
210    fn everything_above_the_empty_node_that_passes_rows_through_goes_with_it() {
211        assert_eq!(
212            pruned(concat!(
213                "Project #1 [#0.0::INTEGER AS a]\n",
214                "  Sort [#0.0::INTEGER ASC NULLS LAST]\n",
215                "    Filter FALSE::BOOLEAN\n",
216                "      Get memory.main.t AS t #0 [a::INTEGER]\n",
217            )),
218            "Values #1 [a::INTEGER] rows=[]\n"
219        );
220    }
221
222    #[test]
223    fn a_limit_of_no_rows_is_an_empty_relation() {
224        assert_eq!(
225            pruned(concat!("Limit 0 offset 0\n", "  Get memory.main.t AS t #0 [a::INTEGER]\n",)),
226            "Values #0 [a::INTEGER] rows=[]\n"
227        );
228        assert_eq!(
229            pruned(concat!(
230                "TopN 0 offset 0 [#0.0::INTEGER ASC NULLS LAST]\n",
231                "  Get memory.main.t AS t #0 [a::INTEGER]\n",
232            )),
233            "Values #0 [a::INTEGER] rows=[]\n"
234        );
235    }
236
237    #[test]
238    fn a_limit_of_one_row_is_not() {
239        let text = concat!("Limit 1 offset 0\n", "  Get memory.main.t AS t #0 [a::INTEGER]\n",);
240        assert_eq!(pruned(text), text);
241    }
242
243    /// The case this pass is written to be careful about. An ungrouped aggregate over no rows
244    /// produces one row, so the empty relation goes under it and the aggregate stays where it is.
245    #[test]
246    fn an_aggregate_over_nothing_still_produces_its_row() {
247        assert_eq!(
248            pruned(concat!(
249                "Aggregate #1 groups=[] aggregates=[count_star()::BIGINT]\n",
250                "  Filter FALSE::BOOLEAN\n",
251                "    Get memory.main.t AS t #0 [a::INTEGER]\n",
252            )),
253            concat!(
254                "Aggregate #1 groups=[] aggregates=[count_star()::BIGINT]\n",
255                "  Values #0 [a::INTEGER] rows=[]\n",
256            )
257        );
258    }
259
260    /// An empty side of a join is left where it is, because a `VALUES` binds to one table index and
261    /// a join produces two, so replacing the join would mean rewriting every binding above it.
262    #[test]
263    fn an_empty_side_of_a_join_stays_a_side_of_the_join() {
264        assert_eq!(
265            pruned(concat!(
266                "Join INNER on=[]\n",
267                "  Filter FALSE::BOOLEAN\n",
268                "    Get memory.main.t AS t #0 [a::INTEGER]\n",
269                "  Get memory.main.u AS u #1 [x::INTEGER]\n",
270            )),
271            concat!(
272                "Join INNER on=[]\n",
273                "  Values #0 [a::INTEGER] rows=[]\n",
274                "  Get memory.main.u AS u #1 [x::INTEGER]\n",
275            )
276        );
277    }
278
279    /// A filter over a join is unsatisfiable and cannot be spelled as an empty relation, so it is
280    /// left alone rather than replaced by something with the wrong bindings.
281    #[test]
282    fn a_false_filter_over_a_join_is_refused_rather_than_guessed_at() {
283        let text = concat!(
284            "Filter FALSE::BOOLEAN\n",
285            "  Join INNER on=[]\n",
286            "    Get memory.main.t AS t #0 [a::INTEGER]\n",
287            "    Get memory.main.u AS u #1 [x::INTEGER]\n",
288        );
289        assert_eq!(pruned(text), text);
290    }
291
292    #[test]
293    fn running_it_twice_is_running_it_once() {
294        let text = concat!(
295            "Project #1 [#0.0::INTEGER AS a]\n",
296            "  Filter FALSE::BOOLEAN\n",
297            "    Get memory.main.t AS t #0 [a::INTEGER]\n",
298        );
299        let once = pruned(text);
300        assert_eq!(pruned(&once), once);
301    }
302}