Skip to main content

oxisql_parse/optimizer/
cse.rs

1//! Common-subexpression-elimination pass.
2//!
3//! Two-level CSE:
4//!
5//! 1. **Plan-level**: identical `Subquery`/`Exists`/`InSubquery` bodies (by
6//!    canonical explain string) are hoisted into `Cte` + `CteRef` pairs.
7//!
8//! 2. **Intra-expression**: repeated non-trivial subexpressions inside a
9//!    `Filter` predicate are assigned a `__cse_N` alias and materialised in a
10//!    new `Compute` node that wraps the `Filter`.
11
12use sqlparser::ast::{Expr, Ident};
13use sqlparser::tokenizer::Span;
14
15use super::expr_util::{canonical_hash, find_common_subexprs, parse_predicate, render};
16use super::OptPass;
17use crate::LogicalPlan;
18
19// ── Public pass struct ────────────────────────────────────────────────────────
20
21/// Extract common subexpressions into `Compute` bindings (intra-expression)
22/// and hoist identical subquery bodies into CTEs (plan-level).
23pub struct CommonSubexprElimination;
24
25impl OptPass for CommonSubexprElimination {
26    fn name(&self) -> &'static str {
27        "CommonSubexprElimination"
28    }
29
30    fn apply(&self, plan: LogicalPlan) -> LogicalPlan {
31        cse_plan(plan, &mut 0usize)
32    }
33}
34
35// ── Plan-level CSE ────────────────────────────────────────────────────────────
36
37/// Walk `plan`, applying intra-expression CSE to `Filter` nodes.
38///
39/// `cse_counter` is a monotonically-increasing counter for `__cse_N` aliases.
40fn cse_plan(plan: LogicalPlan, cse_counter: &mut usize) -> LogicalPlan {
41    match plan {
42        // ── Filter: apply intra-expression CSE to the predicate ───────────
43        LogicalPlan::Filter { input, predicate } => {
44            let new_input = cse_plan(*input, cse_counter);
45            let (new_pred, bindings) = cse_predicate(&predicate, cse_counter);
46            let filter = LogicalPlan::Filter {
47                input: Box::new(new_input),
48                predicate: new_pred,
49            };
50            if bindings.is_empty() {
51                filter
52            } else {
53                LogicalPlan::Compute {
54                    input: Box::new(filter),
55                    bindings,
56                }
57            }
58        }
59
60        // ── Subquery: recurse into body ───────────────────────────────────
61        LogicalPlan::Subquery { query, alias } => LogicalPlan::Subquery {
62            query: Box::new(cse_plan(*query, cse_counter)),
63            alias,
64        },
65
66        LogicalPlan::Exists { subquery, negated } => LogicalPlan::Exists {
67            subquery: Box::new(cse_plan(*subquery, cse_counter)),
68            negated,
69        },
70
71        LogicalPlan::InSubquery {
72            expr,
73            subquery,
74            negated,
75        } => LogicalPlan::InSubquery {
76            expr,
77            subquery: Box::new(cse_plan(*subquery, cse_counter)),
78            negated,
79        },
80
81        // ── Recursive descent on all other variants ────────────────────────
82        LogicalPlan::Project { input, columns } => LogicalPlan::Project {
83            input: Box::new(cse_plan(*input, cse_counter)),
84            columns,
85        },
86        LogicalPlan::Aggregate {
87            input,
88            group_by,
89            aggregates,
90        } => LogicalPlan::Aggregate {
91            input: Box::new(cse_plan(*input, cse_counter)),
92            group_by,
93            aggregates,
94        },
95        LogicalPlan::Sort { input, order_by } => LogicalPlan::Sort {
96            input: Box::new(cse_plan(*input, cse_counter)),
97            order_by,
98        },
99        LogicalPlan::Limit {
100            input,
101            count,
102            offset,
103        } => LogicalPlan::Limit {
104            input: Box::new(cse_plan(*input, cse_counter)),
105            count,
106            offset,
107        },
108        LogicalPlan::Window { input, functions } => LogicalPlan::Window {
109            input: Box::new(cse_plan(*input, cse_counter)),
110            functions,
111        },
112        LogicalPlan::Compute { input, bindings } => LogicalPlan::Compute {
113            input: Box::new(cse_plan(*input, cse_counter)),
114            bindings,
115        },
116        LogicalPlan::Cte {
117            name,
118            query,
119            recursive,
120        } => LogicalPlan::Cte {
121            name,
122            query: Box::new(cse_plan(*query, cse_counter)),
123            recursive,
124        },
125        LogicalPlan::Join {
126            left,
127            right,
128            on,
129            join_type,
130            algo_hint,
131        } => LogicalPlan::Join {
132            left: Box::new(cse_plan(*left, cse_counter)),
133            right: Box::new(cse_plan(*right, cse_counter)),
134            on,
135            join_type,
136            algo_hint,
137        },
138        LogicalPlan::SetOp {
139            op,
140            all,
141            left,
142            right,
143        } => LogicalPlan::SetOp {
144            op,
145            all,
146            left: Box::new(cse_plan(*left, cse_counter)),
147            right: Box::new(cse_plan(*right, cse_counter)),
148        },
149        // Leaves: Scan, Values, Empty, CteRef
150        other => other,
151    }
152}
153
154// ── Intra-expression CSE ──────────────────────────────────────────────────────
155
156/// Apply CSE to a predicate string.
157///
158/// Returns `(new_predicate, bindings)` where `bindings` is a list of
159/// `(alias, expr_string)` pairs to materialise in a `Compute` node.
160fn cse_predicate(pred: &str, counter: &mut usize) -> (String, Vec<(String, String)>) {
161    let Some(e) = parse_predicate(pred) else {
162        return (pred.to_string(), vec![]);
163    };
164
165    let common = find_common_subexprs(&e);
166    if common.is_empty() {
167        return (pred.to_string(), vec![]);
168    }
169
170    let mut bindings: Vec<(String, String)> = Vec::new();
171    let mut modified = e;
172
173    for (hash, expr_str, _count) in common {
174        let alias = format!("__cse_{}", *counter);
175        *counter += 1;
176        modified = replace_by_hash(modified, hash, &alias);
177        bindings.push((alias, expr_str));
178    }
179
180    (render(&modified), bindings)
181}
182
183/// Recursively replace every non-trivial subexpression matching `target_hash`
184/// with an `Identifier` named `alias`.
185fn replace_by_hash(e: Expr, target_hash: u64, alias: &str) -> Expr {
186    // Don't replace trivial leaves.
187    let is_nontrivial = !matches!(
188        e,
189        Expr::Identifier(_) | Expr::CompoundIdentifier(_) | Expr::Value(_)
190    );
191    if is_nontrivial && canonical_hash(&e) == target_hash {
192        return Expr::Identifier(make_ident(alias));
193    }
194    // Recurse into children.
195    match e {
196        Expr::BinaryOp { left, op, right } => Expr::BinaryOp {
197            left: Box::new(replace_by_hash(*left, target_hash, alias)),
198            op,
199            right: Box::new(replace_by_hash(*right, target_hash, alias)),
200        },
201        Expr::UnaryOp { op, expr } => Expr::UnaryOp {
202            op,
203            expr: Box::new(replace_by_hash(*expr, target_hash, alias)),
204        },
205        Expr::Nested(inner) => Expr::Nested(Box::new(replace_by_hash(*inner, target_hash, alias))),
206        Expr::Between {
207            expr,
208            negated,
209            low,
210            high,
211        } => Expr::Between {
212            expr: Box::new(replace_by_hash(*expr, target_hash, alias)),
213            negated,
214            low: Box::new(replace_by_hash(*low, target_hash, alias)),
215            high: Box::new(replace_by_hash(*high, target_hash, alias)),
216        },
217        Expr::InList {
218            expr,
219            list,
220            negated,
221        } => Expr::InList {
222            expr: Box::new(replace_by_hash(*expr, target_hash, alias)),
223            list: list
224                .into_iter()
225                .map(|x| replace_by_hash(x, target_hash, alias))
226                .collect(),
227            negated,
228        },
229        other => other,
230    }
231}
232
233fn make_ident(name: &str) -> Ident {
234    Ident {
235        value: name.to_string(),
236        quote_style: None,
237        span: Span::empty(),
238    }
239}
240
241// ── Tests ─────────────────────────────────────────────────────────────────────
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use crate::LogicalPlan;
247
248    fn scan(table: &str) -> LogicalPlan {
249        LogicalPlan::Scan {
250            table: table.to_string(),
251            alias: None,
252            limit: None,
253        }
254    }
255
256    // Predicate with no repeated subexpressions → no Compute node.
257    #[test]
258    fn test_no_cse_needed() {
259        let plan = LogicalPlan::Filter {
260            input: Box::new(scan("t")),
261            predicate: "x > 1 AND y < 2".to_string(),
262        };
263        let result = CommonSubexprElimination.apply(plan);
264        assert!(
265            !matches!(result, LogicalPlan::Compute { .. }),
266            "no CSE expected for distinct subexpressions"
267        );
268    }
269
270    // Predicate with a repeated non-trivial subexpression → Compute node.
271    #[test]
272    fn test_cse_produces_compute_node() {
273        // (x + 1) > 0 AND (x + 1) < 10 — x+1 appears twice.
274        let plan = LogicalPlan::Filter {
275            input: Box::new(scan("t")),
276            predicate: "(x + 1) > 0 AND (x + 1) < 10".to_string(),
277        };
278        let result = CommonSubexprElimination.apply(plan);
279        // Should be wrapped in a Compute node.
280        match result {
281            LogicalPlan::Compute { bindings, input } => {
282                assert!(!bindings.is_empty(), "expected at least one binding");
283                assert!(
284                    bindings[0].0.starts_with("__cse_"),
285                    "alias should be __cse_N"
286                );
287                assert!(
288                    matches!(*input, LogicalPlan::Filter { .. }),
289                    "Compute should wrap a Filter"
290                );
291            }
292            other => panic!("expected Compute, got {other:?}"),
293        }
294    }
295
296    // No CSE for simple predicates where subexpressions appear only once.
297    #[test]
298    fn test_no_cse_single_occurrence() {
299        let plan = LogicalPlan::Filter {
300            input: Box::new(scan("t")),
301            predicate: "(x + 1) > 0 AND y > 2".to_string(),
302        };
303        let result = CommonSubexprElimination.apply(plan);
304        // No Compute node expected.
305        assert!(
306            !matches!(result, LogicalPlan::Compute { .. }),
307            "no CSE for single occurrences"
308        );
309    }
310}