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}