1use 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
19pub 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
35fn cse_plan(plan: LogicalPlan, cse_counter: &mut usize) -> LogicalPlan {
41 match plan {
42 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 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 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 other => other,
151 }
152}
153
154fn 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
183fn replace_by_hash(e: Expr, target_hash: u64, alias: &str) -> Expr {
186 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 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#[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 #[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 #[test]
272 fn test_cse_produces_compute_node() {
273 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 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 #[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 assert!(
306 !matches!(result, LogicalPlan::Compute { .. }),
307 "no CSE for single occurrences"
308 );
309 }
310}