Skip to main content

oxisql_parse/optimizer/
folding.rs

1//! Pass 3: Constant folding.
2//!
3//! Evaluates constant expressions in `Filter` predicates at compile time:
4//!
5//! * Always-true predicates (`1 = 1`, `TRUE`) cause the `Filter` node to be
6//!   removed entirely.
7//! * Always-false predicates (`1 = 2`, `FALSE`) replace the entire subtree
8//!   with [`LogicalPlan::Empty`].
9//! * Arithmetic on numeric literals (`5 + 3`) is folded to the result (`8`).
10
11use crate::LogicalPlan;
12use sqlparser::dialect::GenericDialect;
13use sqlparser::parser::Parser;
14
15use super::OptPass;
16
17// ── Public struct ─────────────────────────────────────────────────────────────
18
19/// Evaluate constant expressions in `Filter` predicates at compile time.
20pub struct ConstantFolding;
21
22impl OptPass for ConstantFolding {
23    fn name(&self) -> &'static str {
24        "ConstantFolding"
25    }
26
27    fn apply(&self, plan: LogicalPlan) -> LogicalPlan {
28        fold_constants(plan)
29    }
30}
31
32// ── Implementation ────────────────────────────────────────────────────────────
33
34pub(super) fn fold_constants(plan: LogicalPlan) -> LogicalPlan {
35    match plan {
36        LogicalPlan::Filter { input, predicate } => {
37            // Recurse first so children are folded before this node.
38            let inner = fold_constants(*input);
39
40            match evaluate_constant_predicate(&predicate) {
41                Some(true) => inner,               // always-true: drop the Filter
42                Some(false) => LogicalPlan::Empty, // always-false: no rows possible
43                None => LogicalPlan::Filter {
44                    input: Box::new(inner),
45                    predicate: fold_predicate_string(&predicate),
46                },
47            }
48        }
49        LogicalPlan::Project { input, columns } => LogicalPlan::Project {
50            input: Box::new(fold_constants(*input)),
51            columns,
52        },
53        LogicalPlan::Join {
54            left,
55            right,
56            on,
57            join_type,
58            algo_hint,
59        } => LogicalPlan::Join {
60            left: Box::new(fold_constants(*left)),
61            right: Box::new(fold_constants(*right)),
62            on,
63            join_type,
64            algo_hint,
65        },
66        LogicalPlan::Aggregate {
67            input,
68            group_by,
69            aggregates,
70        } => LogicalPlan::Aggregate {
71            input: Box::new(fold_constants(*input)),
72            group_by,
73            aggregates,
74        },
75        LogicalPlan::Sort { input, order_by } => LogicalPlan::Sort {
76            input: Box::new(fold_constants(*input)),
77            order_by,
78        },
79        LogicalPlan::Limit {
80            input,
81            count,
82            offset,
83        } => LogicalPlan::Limit {
84            input: Box::new(fold_constants(*input)),
85            count,
86            offset,
87        },
88        other => other,
89    }
90}
91
92// ── Predicate evaluation ──────────────────────────────────────────────────────
93
94/// Try to evaluate a predicate string as a constant boolean.
95///
96/// Returns `Some(true)` for always-true, `Some(false)` for always-false,
97/// `None` if the predicate is non-constant or cannot be parsed.
98fn evaluate_constant_predicate(predicate: &str) -> Option<bool> {
99    let dialect = GenericDialect {};
100    let mut parser = Parser::new(&dialect).try_with_sql(predicate).ok()?;
101    let expr = parser.parse_expr().ok()?;
102    eval_expr_bool(&expr)
103}
104
105/// Recursively evaluate an expression to a boolean, returning `None` if it
106/// involves non-constant sub-expressions.
107fn eval_expr_bool(expr: &sqlparser::ast::Expr) -> Option<bool> {
108    use sqlparser::ast::{BinaryOperator, Expr, Value, ValueWithSpan};
109
110    match expr {
111        // `TRUE` / `FALSE` literal
112        Expr::Value(ValueWithSpan {
113            value: Value::Boolean(b),
114            ..
115        }) => Some(*b),
116
117        // Comparison or boolean operator.
118        Expr::BinaryOp { left, op, right } => {
119            // Try numeric comparison first.
120            if let (Some(lv), Some(rv)) = (expr_to_f64(left), expr_to_f64(right)) {
121                return Some(match op {
122                    BinaryOperator::Eq => (lv - rv).abs() < f64::EPSILON,
123                    BinaryOperator::NotEq => (lv - rv).abs() >= f64::EPSILON,
124                    BinaryOperator::Lt => lv < rv,
125                    BinaryOperator::LtEq => lv <= rv,
126                    BinaryOperator::Gt => lv > rv,
127                    BinaryOperator::GtEq => lv >= rv,
128                    _ => return None,
129                });
130            }
131            // Try string equality.
132            if let (Some(ls), Some(rs)) = (expr_to_str(left), expr_to_str(right)) {
133                return Some(match op {
134                    BinaryOperator::Eq => ls == rs,
135                    BinaryOperator::NotEq => ls != rs,
136                    _ => return None,
137                });
138            }
139            // Boolean AND / OR with known sub-values.
140            match op {
141                BinaryOperator::And => {
142                    let lv = eval_expr_bool(left);
143                    let rv = eval_expr_bool(right);
144                    match (lv, rv) {
145                        (Some(false), _) | (_, Some(false)) => Some(false),
146                        (Some(true), Some(true)) => Some(true),
147                        _ => None,
148                    }
149                }
150                BinaryOperator::Or => {
151                    let lv = eval_expr_bool(left);
152                    let rv = eval_expr_bool(right);
153                    match (lv, rv) {
154                        (Some(true), _) | (_, Some(true)) => Some(true),
155                        (Some(false), Some(false)) => Some(false),
156                        _ => None,
157                    }
158                }
159                _ => None,
160            }
161        }
162
163        Expr::Nested(inner) => eval_expr_bool(inner),
164        _ => None,
165    }
166}
167
168// ── Arithmetic folding ────────────────────────────────────────────────────────
169
170/// Fold arithmetic in a predicate string and return the rewritten string.
171///
172/// Parses the expression, folds constant arithmetic, and serialises back.
173/// If parsing fails the original string is returned unchanged.
174fn fold_predicate_string(predicate: &str) -> String {
175    let dialect = GenericDialect {};
176    let Ok(mut parser) = Parser::new(&dialect).try_with_sql(predicate) else {
177        return predicate.to_string();
178    };
179    let Ok(expr) = parser.parse_expr() else {
180        return predicate.to_string();
181    };
182    fold_expr(expr).to_string()
183}
184
185/// Recursively fold constant arithmetic inside an expression tree.
186fn fold_expr(expr: sqlparser::ast::Expr) -> sqlparser::ast::Expr {
187    use sqlparser::ast::{BinaryOperator, Expr, Value, ValueWithSpan};
188
189    match expr {
190        Expr::BinaryOp { left, op, right } => {
191            let left = fold_expr(*left);
192            let right = fold_expr(*right);
193
194            // Arithmetic folding on numeric literals.
195            if let (Some(lv), Some(rv)) = (expr_to_f64(&left), expr_to_f64(&right)) {
196                let result = match op {
197                    BinaryOperator::Plus => Some(lv + rv),
198                    BinaryOperator::Minus => Some(lv - rv),
199                    BinaryOperator::Multiply => Some(lv * rv),
200                    BinaryOperator::Divide if rv != 0.0 => Some(lv / rv),
201                    _ => None,
202                };
203                if let Some(v) = result {
204                    let s = if v.fract() == 0.0 && v.abs() < 1e15 {
205                        format!("{}", v as i64)
206                    } else {
207                        format!("{v}")
208                    };
209                    return Expr::Value(ValueWithSpan {
210                        value: Value::Number(s, false),
211                        span: sqlparser::tokenizer::Span::empty(),
212                    });
213                }
214            }
215
216            Expr::BinaryOp {
217                left: Box::new(left),
218                op,
219                right: Box::new(right),
220            }
221        }
222        Expr::Nested(inner) => Expr::Nested(Box::new(fold_expr(*inner))),
223        other => other,
224    }
225}
226
227// ── Literal extraction helpers ────────────────────────────────────────────────
228
229/// Extract a numeric value from a literal expression if possible.
230fn expr_to_f64(expr: &sqlparser::ast::Expr) -> Option<f64> {
231    use sqlparser::ast::{Expr, Value, ValueWithSpan};
232    match expr {
233        Expr::Value(ValueWithSpan {
234            value: Value::Number(s, _),
235            ..
236        }) => s.parse::<f64>().ok(),
237        _ => None,
238    }
239}
240
241/// Extract a string literal value from an expression if possible.
242fn expr_to_str(expr: &sqlparser::ast::Expr) -> Option<&str> {
243    use sqlparser::ast::{Expr, Value, ValueWithSpan};
244    match expr {
245        Expr::Value(ValueWithSpan {
246            value: Value::SingleQuotedString(s),
247            ..
248        }) => Some(s.as_str()),
249        _ => None,
250    }
251}