Skip to main content

rudb_opt/
fold.rs

1//! Constant folding and the simplifications that fall out of it.
2//!
3//! DuckDB calls this pass `expression_rewriter` and so does rudb, because a corpus file that says
4//! `SET disabled_optimizers = 'expression_rewriter'` was written to turn this off. It is a subset
5//! of what the binary's rewriter does. What is here is folding, the conjunction rules, the `CASE`
6//! rules and the comparison against a null, all of which were read off the pinned binary rather
7//! than reasoned about. What is not here is arithmetic simplification, which turns `i + 0` into `i`,
8//! and reassociation, which turns `3 + 4 + i` into `7 + i`. Both are worth having and neither is
9//! folding, so both are a later pull request.
10//!
11//! `spec/engine/11-optimizer.md` section 11.2 puts this fifth by value, worth tens of percent
12//! rather than multiples, and then gives the reason it lands earlier than that: folding is required
13//! for correctness in a few places anyway. A `WHERE false` that is still an expression is a table
14//! scan, and the pass that removes the scan can only see that the predicate is false once something
15//! has made it false.
16//!
17//! # What is not folded, and why
18//!
19//! A fold that raises is abandoned and the expression is left exactly as it was written. `CAST('abc'
20//! AS INTEGER)` stays a cast, so the error still comes from running the query rather than from
21//! planning it, and a query whose unreachable branch would have raised still runs. The binary does
22//! the same thing and it is the only safe rule: an optimizer that can turn a query that returns rows
23//! into a query that returns an error is an optimizer that changes answers.
24//!
25//! A volatile function is not folded. There are none in rudb yet, so [`VOLATILE`] is a list of names
26//! nothing answers to, and that is on purpose. The day `random()` lands, a pass that folded it would
27//! give every row the same number, and the version of this file that grows the list at the same time
28//! as the function is the version where somebody has to remember.
29//!
30//! A fold whose value does not have the type the plan recorded for the expression is abandoned too.
31//! That cannot happen if the kernels and the binder agree, which is the point: it is a disagreement
32//! between the two, and turning it into a plan that still runs correctly is better than turning it
33//! into a validation failure a long way from the cause.
34
35use std::collections::HashMap;
36
37use rudb_common::{LogicalType, Result, Value};
38use rudb_kernels::{Comparison, Connective, call_values, cast_value, combine, compare_values};
39use rudb_plan::{CompareOp, ConjunctionOp, Expr, ExprRef, Node, NodeRef, Plan, Slice, SortKey};
40use rudb_vector::Vector;
41
42use crate::pass::{Context, Pass, top_down};
43use crate::walk;
44
45/// The functions whose value is not decided by their arguments.
46///
47/// `SELECT DISTINCT function_name FROM duckdb_functions() WHERE has_side_effects` on the pinned
48/// binary, which is the list at the commit the grammar is vendored from. rudb implements none of
49/// them today and the list is here anyway, so that the first one to land is refused by a pass that
50/// already knew about it rather than folded by a pass that had never heard of it.
51pub const VOLATILE: [&str; 17] = [
52    "current_connection_id",
53    "current_query",
54    "current_query_id",
55    "current_transaction_id",
56    "currval",
57    "error",
58    "gen_random_uuid",
59    "nextval",
60    "random",
61    "setseed",
62    "setval",
63    "sleep_ms",
64    "stats",
65    "uuid",
66    "uuidv4",
67    "uuidv7",
68    "write_log",
69];
70
71/// Folds what can be folded and simplifies what folding exposes.
72#[derive(Debug, Clone, Copy)]
73pub struct ExpressionRewriter;
74
75impl Pass for ExpressionRewriter {
76    fn name(&self) -> &'static str {
77        "expression_rewriter"
78    }
79
80    fn run(&self, plan: &mut Plan, _context: &Context) -> Result<()> {
81        rewrite(plan);
82        Ok(())
83    }
84}
85
86/// What each expression has already been rewritten to.
87///
88/// The arena shares operands, so one expression is reached from as many places as refer to it, and
89/// rewriting it once per reference would turn a shared subtree into as many copies as there are
90/// references. Sharing it afterwards is not only about the arena's size: the prepared form in the
91/// executor keys its common subexpressions by reference, so a tree that arrives unshared runs the
92/// same work twice.
93type Done = HashMap<ExprRef, ExprRef>;
94
95/// Rewrites every expression the plan reaches.
96fn rewrite(plan: &mut Plan) {
97    let mut done = Done::new();
98    for node in top_down(plan) {
99        node_expressions(plan, node, &mut done);
100    }
101}
102
103/// Rewrites the expressions one node holds, writing back only the slots that changed.
104///
105/// Only what changed, because a pool entry is appended rather than overwritten and a pass that
106/// rebuilt every slice would grow the arena by the size of the plan every time it ran. Running it
107/// twice has to be running it once, which is what the idempotence assertion in
108/// `spec/09-optimizer.md` section 9.1 asks of every pass.
109fn node_expressions(plan: &mut Plan, node: NodeRef, done: &mut Done) {
110    match *plan.node(node) {
111        Node::Get { .. }
112        | Node::Dummy
113        | Node::Limit { .. }
114        | Node::SetOp { .. }
115        | Node::CrossProduct { .. } => {}
116        Node::Values { rows, .. } => {
117            let held = plan.row_list(rows).to_vec();
118            let rewritten: Vec<Slice> =
119                held.iter().map(|&row| expr_list(plan, row, done).unwrap_or(row)).collect();
120            if rewritten != held {
121                let rows = plan.add_rows(&rewritten);
122                match plan.node_mut(node) {
123                    Node::Values { rows: held, .. } => *held = rows,
124                    _ => unreachable!("the node was a values list a moment ago"),
125                }
126            }
127        }
128        Node::TableFunction { args, .. } => {
129            if let Some(rewritten) = expr_list(plan, args, done) {
130                match plan.node_mut(node) {
131                    Node::TableFunction { args, .. } => *args = rewritten,
132                    _ => unreachable!("the node was a table function a moment ago"),
133                }
134            }
135        }
136        Node::Filter { predicate, .. } => {
137            let rewritten = expression(plan, predicate, done);
138            if rewritten != predicate {
139                match plan.node_mut(node) {
140                    Node::Filter { predicate, .. } => *predicate = rewritten,
141                    _ => unreachable!("the node was a filter a moment ago"),
142                }
143            }
144        }
145        Node::Project { exprs, .. } => {
146            if let Some(rewritten) = expr_list(plan, exprs, done) {
147                match plan.node_mut(node) {
148                    Node::Project { exprs, .. } => *exprs = rewritten,
149                    _ => unreachable!("the node was a projection a moment ago"),
150                }
151            }
152        }
153        Node::Aggregate { groups, aggregates, .. } => {
154            let rewritten_groups = expr_list(plan, groups, done);
155            let rewritten_aggregates = expr_list(plan, aggregates, done);
156            match plan.node_mut(node) {
157                Node::Aggregate { groups, aggregates, .. } => {
158                    if let Some(rewritten) = rewritten_groups {
159                        *groups = rewritten;
160                    }
161                    if let Some(rewritten) = rewritten_aggregates {
162                        *aggregates = rewritten;
163                    }
164                }
165                _ => unreachable!("the node was an aggregate a moment ago"),
166            }
167        }
168        Node::Sort { keys, .. } | Node::TopN { keys, .. } => {
169            let held = plan.sort_key_list(keys).to_vec();
170            let rewritten: Vec<SortKey> = held
171                .iter()
172                .map(|key| SortKey { expr: expression(plan, key.expr, done), ..*key })
173                .collect();
174            if rewritten != held {
175                let keys = plan.add_sort_keys(&rewritten);
176                match plan.node_mut(node) {
177                    Node::Sort { keys: held, .. } | Node::TopN { keys: held, .. } => *held = keys,
178                    _ => unreachable!("the node was a sort a moment ago"),
179                }
180            }
181        }
182        Node::Distinct { on, .. } => {
183            if let Some(rewritten) = expr_list(plan, on, done) {
184                match plan.node_mut(node) {
185                    Node::Distinct { on, .. } => *on = rewritten,
186                    _ => unreachable!("the node was a distinct a moment ago"),
187                }
188            }
189        }
190        Node::Join { conditions, .. } => {
191            if let Some(rewritten) = expr_list(plan, conditions, done) {
192                match plan.node_mut(node) {
193                    Node::Join { conditions, .. } => *conditions = rewritten,
194                    _ => unreachable!("the node was a join a moment ago"),
195                }
196            }
197        }
198    }
199}
200
201/// Rewrites a run of expressions, handing back a new slice only if one of them changed.
202fn expr_list(plan: &mut Plan, slice: Slice, done: &mut Done) -> Option<Slice> {
203    walk::list(plan, slice, &mut |plan, expr| expression(plan, expr, done))
204}
205
206/// Rewrites one expression and everything under it, bottom up.
207///
208/// Bottom up is not a preference. An expression may only refer to an expression behind it in the
209/// arena, which `Plan::validate` checks and which is what makes a plan acyclic by construction, so a
210/// rewritten operand has to be appended before the operator that reads it. Folding a child first is
211/// also what makes one pass enough: `1 + 2 + 3` folds to `6` in a single walk because by the time
212/// the outer call runs, its operand is already a constant.
213fn expression(plan: &mut Plan, expr: ExprRef, done: &mut Done) -> ExprRef {
214    if let Some(&already) = done.get(&expr) {
215        return already;
216    }
217    let rebuilt = walk::rebuild(plan, expr, &mut |plan, child| expression(plan, child, done));
218    let simplified = simplify(plan, rebuilt);
219    done.insert(expr, simplified);
220    simplified
221}
222
223/// Applies every rule to one expression whose operands are already rewritten.
224fn simplify(plan: &mut Plan, expr: ExprRef) -> ExprRef {
225    if matches!(*plan.expr(expr), Expr::Constant(_)) {
226        return expr;
227    }
228    if let Some(value) = fold(plan, expr) {
229        if let Some(folded) = constant_of(plan, expr, value) {
230            return folded;
231        }
232    }
233    match *plan.expr(expr) {
234        Expr::Conjunction { op, children } => conjunction(plan, expr, op, children),
235        Expr::Case { arms, otherwise } => case(plan, expr, arms, otherwise),
236        Expr::Compare { op, left, right } => null_comparison(plan, expr, op, left, right),
237        _ => expr,
238    }
239}
240
241/// The value of an expression all of whose operands are constants, if it has one.
242///
243/// One level deep, because the operands have already been through this and a foldable one is
244/// already a constant. The kernels it calls are the ones the executor calls for the same
245/// expression, which is what makes a folded answer and a computed answer the same answer by
246/// construction rather than by testing every function twice.
247fn fold(plan: &Plan, expr: ExprRef) -> Option<Value> {
248    match *plan.expr(expr) {
249        Expr::Cast { input, try_cast } => {
250            let inner = constant(plan, input)?;
251            cast_value(&inner, plan.expr_type(expr), try_cast).ok()
252        }
253        Expr::Compare { op, left, right } => {
254            let left = constant(plan, left)?;
255            let right = constant(plan, right)?;
256            compare_values(comparison(op), &left, &right).ok()
257        }
258        Expr::Conjunction { op, children } => {
259            let values = constants(plan, children)?;
260            let vectors: Vec<Vector> = values
261                .into_iter()
262                .map(|value| Vector::constant(LogicalType::Boolean, value, 1))
263                .collect();
264            Some(combine(connective(op), &vectors).ok()?.value_at(0))
265        }
266        Expr::Function { name, args } => {
267            let name = plan.string(name);
268            if VOLATILE.contains(&name) {
269                return None;
270            }
271            let values = constants(plan, args)?;
272            // No expression to name, because nothing here keeps the error: a call that fails is a
273            // call that does not fold, the node stays in the plan, and the executor raises the same
274            // failure later with the expression to hand. `7 // 0` is that, and the message a user
275            // sees for it comes from the executor and not from here.
276            call_values(name, &values, plan.expr_type(expr), None).ok()
277        }
278        _ => None,
279    }
280}
281
282/// The value behind an expression, if it is a constant.
283fn constant(plan: &Plan, expr: ExprRef) -> Option<Value> {
284    match *plan.expr(expr) {
285        Expr::Constant(value) => Some(plan.value(value).clone()),
286        _ => None,
287    }
288}
289
290/// The values behind a run of expressions, if every one of them is a constant.
291fn constants(plan: &Plan, slice: Slice) -> Option<Vec<Value>> {
292    plan.expr_list(slice).iter().map(|&expr| constant(plan, expr)).collect()
293}
294
295/// A constant expression holding `value`, keeping the type the expression already had.
296///
297/// The expression's own type and not the value's, because a null carries no type and the plan says
298/// what the column is. `None` if the two disagree about anything else, which is the abandoned fold
299/// the module documentation describes.
300fn constant_of(plan: &mut Plan, expr: ExprRef, value: Value) -> Option<ExprRef> {
301    let ty = plan.expr_type(expr).clone();
302    if !value.is_null() && value.logical_type() != ty {
303        return None;
304    }
305    let held = plan.add_value(value);
306    Some(plan.add_expr(Expr::Constant(held), ty))
307}
308
309/// `x AND true` is `x`, `x AND false` is `false`, and the same the other way up for `OR`.
310///
311/// A null operand is kept rather than dropped, because `x AND NULL` is null where `x` is true and
312/// false where `x` is false, so it is neither the operand nor a constant. The binary keeps it too.
313fn conjunction(plan: &mut Plan, expr: ExprRef, op: ConjunctionOp, children: Slice) -> ExprRef {
314    // The value that decides the whole connective on its own, and the one that drops out of it.
315    let (decides, drops) = match op {
316        ConjunctionOp::And => (false, true),
317        ConjunctionOp::Or => (true, false),
318    };
319    let held = plan.expr_list(children).to_vec();
320    let mut kept = Vec::with_capacity(held.len());
321    for child in held.iter().copied() {
322        match constant(plan, child).as_ref().and_then(Value::as_bool) {
323            Some(known) if known == decides => {
324                return constant_of(plan, expr, Value::Boolean(decides)).unwrap_or(expr);
325            }
326            Some(_) => {}
327            None => kept.push(child),
328        }
329    }
330    if kept.len() == held.len() {
331        return expr;
332    }
333    match kept.as_slice() {
334        [] => constant_of(plan, expr, Value::Boolean(drops)).unwrap_or(expr),
335        [only] => *only,
336        rest => {
337            let children = plan.add_expr_list(rest);
338            plan.add_expr(Expr::Conjunction { op, children }, LogicalType::Boolean)
339        }
340    }
341}
342
343/// Whether an arm's condition is decided before the query runs.
344enum Fires {
345    /// The condition is a true constant, so this arm is the answer and the ones after it are not.
346    Always,
347    /// The condition is a false or null constant. A null condition does not fire, which is the same
348    /// rule `WHERE` uses and is why there is one variant for the two.
349    Never,
350    /// The condition depends on the row.
351    Maybe,
352}
353
354fn fires(plan: &Plan, when: ExprRef) -> Fires {
355    match constant(plan, when) {
356        Some(Value::Boolean(true)) => Fires::Always,
357        Some(Value::Boolean(false) | Value::Null) => Fires::Never,
358        // A condition that is a constant of some other type is a malformed plan, and this is not
359        // where that gets reported. `Plan::validate` says so with the expression number.
360        Some(_) | None => Fires::Maybe,
361    }
362}
363
364/// Drops the arms that cannot fire and cuts the `CASE` at the first one that always does.
365fn case(plan: &mut Plan, expr: ExprRef, arms: Slice, otherwise: Option<ExprRef>) -> ExprRef {
366    let held = plan.arm_list(arms).to_vec();
367    let mut kept = Vec::with_capacity(held.len());
368    let mut result = otherwise;
369    let mut cut = false;
370    for arm in held.iter().copied() {
371        match fires(plan, arm.when) {
372            Fires::Never => {}
373            Fires::Always => {
374                result = Some(arm.then);
375                cut = true;
376                break;
377            }
378            Fires::Maybe => kept.push(arm),
379        }
380    }
381    if kept.len() == held.len() && !cut {
382        return expr;
383    }
384    if kept.is_empty() {
385        // Every condition was decided, so the `CASE` is whichever branch was left standing. A
386        // branch whose type is not the `CASE`'s own would change what the column is, which the
387        // binder never builds and which is left alone rather than guessed at.
388        return match result {
389            Some(only) if plan.expr_type(only) == plan.expr_type(expr) => only,
390            Some(_) => expr,
391            None => constant_of(plan, expr, Value::Null).unwrap_or(expr),
392        };
393    }
394    let ty = plan.expr_type(expr).clone();
395    let arms = plan.add_arms(&kept);
396    plan.add_expr(Expr::Case { arms, otherwise: result }, ty)
397}
398
399/// A comparison against a null constant is null, whatever the other side is.
400///
401/// Not for `IS DISTINCT FROM` and `IS NOT DISTINCT FROM`, which are the two that have an answer
402/// when an operand is null and are the reason anybody writes them.
403///
404/// It drops the other side rather than evaluating it, so an expression that would have raised no
405/// longer does. That is what the binary does with the same query, and the alternative is keeping a
406/// comparison whose answer is known so that its operand can fail.
407fn null_comparison(
408    plan: &mut Plan,
409    expr: ExprRef,
410    op: CompareOp,
411    left: ExprRef,
412    right: ExprRef,
413) -> ExprRef {
414    if matches!(op, CompareOp::DistinctFrom | CompareOp::NotDistinctFrom) {
415        return expr;
416    }
417    let is_null = |side| constant(plan, side).is_some_and(|value| value.is_null());
418    if is_null(left) || is_null(right) {
419        constant_of(plan, expr, Value::Null).unwrap_or(expr)
420    } else {
421        expr
422    }
423}
424
425/// The kernels' comparison for the plan's.
426///
427/// The same eight arms as the copy in `rudb-exec`, which is where the executor's is. One copy would
428/// have to live in the crate both can see, and that is `rudb-plan`, which does not depend on the
429/// kernels and should not: a plan is a data structure and the day it needs a kernel library to be
430/// constructed is the day nothing can hold a plan without linking the arithmetic. Both matches are
431/// exhaustive, so a ninth comparison stops both of them compiling rather than quietly folding to
432/// the wrong answer in one.
433fn comparison(op: CompareOp) -> Comparison {
434    match op {
435        CompareOp::Equal => Comparison::Equal,
436        CompareOp::NotEqual => Comparison::NotEqual,
437        CompareOp::Less => Comparison::Less,
438        CompareOp::LessOrEqual => Comparison::LessOrEqual,
439        CompareOp::Greater => Comparison::Greater,
440        CompareOp::GreaterOrEqual => Comparison::GreaterOrEqual,
441        CompareOp::DistinctFrom => Comparison::DistinctFrom,
442        CompareOp::NotDistinctFrom => Comparison::NotDistinctFrom,
443    }
444}
445
446/// The kernels' connective for the plan's.
447fn connective(op: ConjunctionOp) -> Connective {
448    match op {
449        ConjunctionOp::And => Connective::And,
450        ConjunctionOp::Or => Connective::Or,
451    }
452}
453
454#[cfg(test)]
455mod tests {
456    use super::{ExpressionRewriter, VOLATILE};
457    use crate::pass::{Context, Pass};
458    use rudb_plan::Plan;
459
460    /// The plan a text prints as after folding, which is what every assertion here reads.
461    fn folded(text: &str) -> String {
462        let mut plan =
463            Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
464        ExpressionRewriter
465            .run(&mut plan, &Context::new())
466            .unwrap_or_else(|error| panic!("{text} did not fold: {error}"));
467        plan.validate().unwrap_or_else(|error| panic!("{text} folded to a bad plan: {error}"));
468        plan.to_string()
469    }
470
471    const SCAN: &str = "  Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR, c::BOOLEAN]\n";
472
473    #[test]
474    fn arithmetic_over_constants_becomes_the_number() {
475        let before = format!("Project #1 [\"+\"(2::INTEGER, 3::INTEGER)::INTEGER AS n]\n{SCAN}");
476        let after = format!("Project #1 [5::INTEGER AS n]\n{SCAN}");
477        assert_eq!(folded(&before), after);
478    }
479
480    #[test]
481    fn a_nest_of_constants_folds_all_the_way_up_in_one_walk() {
482        // What makes one pass enough. By the time the outer call is looked at, its operand is
483        // already a constant, so there is nothing to run a second time over.
484        let before = format!(
485            "Project #1 [\"+\"(\"+\"(1::INTEGER, 2::INTEGER)::INTEGER, 3::INTEGER)::INTEGER AS n]\n{SCAN}"
486        );
487        let after = format!("Project #1 [6::INTEGER AS n]\n{SCAN}");
488        assert_eq!(folded(&before), after);
489    }
490
491    #[test]
492    fn a_call_with_a_column_in_it_is_left_alone() {
493        let text = format!("Project #1 [\"+\"(#0.0::INTEGER, 3::INTEGER)::INTEGER AS n]\n{SCAN}");
494        assert_eq!(folded(&text), text);
495    }
496
497    #[test]
498    fn a_cast_of_a_constant_folds_and_one_that_would_raise_does_not() {
499        let before = format!("Project #1 [CAST('1'::VARCHAR)::INTEGER AS n]\n{SCAN}");
500        let after = format!("Project #1 [1::INTEGER AS n]\n{SCAN}");
501        assert_eq!(folded(&before), after);
502        // The rule the whole pass rests on. The error still comes from running the query, so a
503        // query whose unreachable branch would have raised still runs.
504        let raises = format!("Project #1 [CAST('abc'::VARCHAR)::INTEGER AS n]\n{SCAN}");
505        assert_eq!(folded(&raises), raises);
506    }
507
508    #[test]
509    fn a_comparison_of_constants_becomes_a_boolean() {
510        let before = format!("Filter (1::INTEGER < 2::INTEGER)::BOOLEAN\n{SCAN}");
511        let after = format!("Filter TRUE::BOOLEAN\n{SCAN}");
512        assert_eq!(folded(&before), after);
513    }
514
515    #[test]
516    fn a_comparison_against_a_null_is_null_and_the_other_side_goes_with_it() {
517        let before = format!("Filter (#0.0::INTEGER = NULL::INTEGER)::BOOLEAN\n{SCAN}");
518        let after = format!("Filter NULL::BOOLEAN\n{SCAN}");
519        assert_eq!(folded(&before), after);
520    }
521
522    #[test]
523    fn the_two_comparisons_that_have_an_answer_over_a_null_keep_it() {
524        // `IS NOT DISTINCT FROM NULL` is a test for null and answers true or false, which is the
525        // reason anybody writes it, so the rule above must not reach it.
526        let text =
527            format!("Filter (#0.0::INTEGER IS NOT DISTINCT FROM NULL::INTEGER)::BOOLEAN\n{SCAN}");
528        assert_eq!(folded(&text), text);
529    }
530
531    #[test]
532    fn a_true_drops_out_of_an_and_and_a_false_decides_it() {
533        let before = format!("Filter (TRUE::BOOLEAN AND #0.2::BOOLEAN)::BOOLEAN\n{SCAN}");
534        let after = format!("Filter #0.2::BOOLEAN\n{SCAN}");
535        assert_eq!(folded(&before), after);
536        let decided = format!("Filter (FALSE::BOOLEAN AND #0.2::BOOLEAN)::BOOLEAN\n{SCAN}");
537        let all = format!("Filter FALSE::BOOLEAN\n{SCAN}");
538        assert_eq!(folded(&decided), all);
539    }
540
541    #[test]
542    fn a_false_drops_out_of_an_or_and_a_true_decides_it() {
543        let before = format!("Filter (FALSE::BOOLEAN OR #0.2::BOOLEAN)::BOOLEAN\n{SCAN}");
544        let after = format!("Filter #0.2::BOOLEAN\n{SCAN}");
545        assert_eq!(folded(&before), after);
546        let decided = format!("Filter (TRUE::BOOLEAN OR #0.2::BOOLEAN)::BOOLEAN\n{SCAN}");
547        let all = format!("Filter TRUE::BOOLEAN\n{SCAN}");
548        assert_eq!(folded(&decided), all);
549    }
550
551    #[test]
552    fn a_null_operand_of_an_and_is_kept_because_it_is_neither_the_answer_nor_the_operand() {
553        let text = format!("Filter (NULL::BOOLEAN AND #0.2::BOOLEAN)::BOOLEAN\n{SCAN}");
554        assert_eq!(folded(&text), text);
555    }
556
557    #[test]
558    fn a_conjunction_of_constants_is_the_three_valued_answer() {
559        // `NULL AND false` is false and `NULL OR true` is true, which is the part a rule that only
560        // looked at the nulls would get wrong.
561        let before = format!("Filter (NULL::BOOLEAN AND FALSE::BOOLEAN)::BOOLEAN\n{SCAN}");
562        let after = format!("Filter FALSE::BOOLEAN\n{SCAN}");
563        assert_eq!(folded(&before), after);
564        let other = format!("Filter (NULL::BOOLEAN OR TRUE::BOOLEAN)::BOOLEAN\n{SCAN}");
565        let answer = format!("Filter TRUE::BOOLEAN\n{SCAN}");
566        assert_eq!(folded(&other), answer);
567    }
568
569    #[test]
570    fn a_long_conjunction_keeps_the_operands_that_are_not_decided() {
571        let before = format!(
572            "Filter (#0.2::BOOLEAN AND TRUE::BOOLEAN AND (#0.0::INTEGER > 1::INTEGER)::BOOLEAN)::BOOLEAN\n{SCAN}"
573        );
574        let after = format!(
575            "Filter (#0.2::BOOLEAN AND (#0.0::INTEGER > 1::INTEGER)::BOOLEAN)::BOOLEAN\n{SCAN}"
576        );
577        assert_eq!(folded(&before), after);
578    }
579
580    #[test]
581    fn an_arm_that_cannot_fire_is_dropped_and_a_null_condition_is_one_of_them() {
582        let before = format!(
583            "Project #1 [CASE WHEN FALSE::BOOLEAN THEN 1::INTEGER ELSE #0.0::INTEGER END::INTEGER AS n]\n{SCAN}"
584        );
585        let after = format!("Project #1 [#0.0::INTEGER AS n]\n{SCAN}");
586        assert_eq!(folded(&before), after);
587        let null = format!(
588            "Project #1 [CASE WHEN NULL::BOOLEAN THEN 1::INTEGER ELSE #0.0::INTEGER END::INTEGER AS n]\n{SCAN}"
589        );
590        assert_eq!(folded(&null), after);
591    }
592
593    #[test]
594    fn the_first_arm_that_always_fires_cuts_the_ones_after_it() {
595        let before = format!(
596            "Project #1 [CASE WHEN #0.2::BOOLEAN THEN 1::INTEGER WHEN TRUE::BOOLEAN THEN 2::INTEGER WHEN #0.2::BOOLEAN THEN 3::INTEGER ELSE 4::INTEGER END::INTEGER AS n]\n{SCAN}"
597        );
598        let after = format!(
599            "Project #1 [CASE WHEN #0.2::BOOLEAN THEN 1::INTEGER ELSE 2::INTEGER END::INTEGER AS n]\n{SCAN}"
600        );
601        assert_eq!(folded(&before), after);
602    }
603
604    #[test]
605    fn a_case_with_no_arm_left_and_no_else_is_null() {
606        let before = format!(
607            "Project #1 [CASE WHEN FALSE::BOOLEAN THEN 1::INTEGER END::INTEGER AS n]\n{SCAN}"
608        );
609        let after = format!("Project #1 [NULL::INTEGER AS n]\n{SCAN}");
610        assert_eq!(folded(&before), after);
611    }
612
613    #[test]
614    fn an_aggregate_keeps_its_place_and_its_arguments_are_folded_under_it() {
615        // An aggregate may only appear as a direct element of the aggregate list, so folding must
616        // rebuild one rather than replace it, however constant its argument is.
617        let before = format!(
618            "Aggregate #1 groups=[] aggregates=[sum(\"+\"(1::INTEGER, 2::INTEGER)::INTEGER)::HUGEINT]\n{SCAN}"
619        );
620        let after = format!("Aggregate #1 groups=[] aggregates=[sum(3::INTEGER)::HUGEINT]\n{SCAN}");
621        assert_eq!(folded(&before), after);
622    }
623
624    #[test]
625    fn a_sort_key_and_a_join_condition_are_folded_too() {
626        let before =
627            format!("Sort [\"+\"(1::INTEGER, 1::INTEGER)::INTEGER ASC NULLS LAST]\n{SCAN}");
628        let after = format!("Sort [2::INTEGER ASC NULLS LAST]\n{SCAN}");
629        assert_eq!(folded(&before), after);
630    }
631
632    #[test]
633    fn folding_twice_is_folding_once() {
634        let before = format!(
635            "Filter (TRUE::BOOLEAN AND (\"+\"(1::INTEGER, 1::INTEGER)::INTEGER > #0.0::INTEGER)::BOOLEAN)::BOOLEAN\n{SCAN}"
636        );
637        let once = folded(&before);
638        assert_eq!(folded(&once), once);
639    }
640
641    #[test]
642    fn a_volatile_call_is_not_folded_however_constant_its_arguments_are() {
643        // There is no `random` in rudb yet, so this asserts the list rather than the function. The
644        // day one lands, a pass that folded it would give every row the same number.
645        assert!(VOLATILE.contains(&"random"));
646        assert!(VOLATILE.contains(&"nextval"));
647        let text = format!("Project #1 [random()::DOUBLE AS n]\n{SCAN}");
648        assert_eq!(folded(&text), text);
649    }
650}