use sqlparser::ast::{Expr, Ident};
use sqlparser::tokenizer::Span;
use super::expr_util::{canonical_hash, find_common_subexprs, parse_predicate, render};
use super::OptPass;
use crate::LogicalPlan;
pub struct CommonSubexprElimination;
impl OptPass for CommonSubexprElimination {
fn name(&self) -> &'static str {
"CommonSubexprElimination"
}
fn apply(&self, plan: LogicalPlan) -> LogicalPlan {
cse_plan(plan, &mut 0usize)
}
}
fn cse_plan(plan: LogicalPlan, cse_counter: &mut usize) -> LogicalPlan {
match plan {
LogicalPlan::Filter { input, predicate } => {
let new_input = cse_plan(*input, cse_counter);
let (new_pred, bindings) = cse_predicate(&predicate, cse_counter);
let filter = LogicalPlan::Filter {
input: Box::new(new_input),
predicate: new_pred,
};
if bindings.is_empty() {
filter
} else {
LogicalPlan::Compute {
input: Box::new(filter),
bindings,
}
}
}
LogicalPlan::Subquery { query, alias } => LogicalPlan::Subquery {
query: Box::new(cse_plan(*query, cse_counter)),
alias,
},
LogicalPlan::Exists { subquery, negated } => LogicalPlan::Exists {
subquery: Box::new(cse_plan(*subquery, cse_counter)),
negated,
},
LogicalPlan::InSubquery {
expr,
subquery,
negated,
} => LogicalPlan::InSubquery {
expr,
subquery: Box::new(cse_plan(*subquery, cse_counter)),
negated,
},
LogicalPlan::Project { input, columns } => LogicalPlan::Project {
input: Box::new(cse_plan(*input, cse_counter)),
columns,
},
LogicalPlan::Aggregate {
input,
group_by,
aggregates,
} => LogicalPlan::Aggregate {
input: Box::new(cse_plan(*input, cse_counter)),
group_by,
aggregates,
},
LogicalPlan::Sort { input, order_by } => LogicalPlan::Sort {
input: Box::new(cse_plan(*input, cse_counter)),
order_by,
},
LogicalPlan::Limit {
input,
count,
offset,
} => LogicalPlan::Limit {
input: Box::new(cse_plan(*input, cse_counter)),
count,
offset,
},
LogicalPlan::Window { input, functions } => LogicalPlan::Window {
input: Box::new(cse_plan(*input, cse_counter)),
functions,
},
LogicalPlan::Compute { input, bindings } => LogicalPlan::Compute {
input: Box::new(cse_plan(*input, cse_counter)),
bindings,
},
LogicalPlan::Cte {
name,
query,
recursive,
} => LogicalPlan::Cte {
name,
query: Box::new(cse_plan(*query, cse_counter)),
recursive,
},
LogicalPlan::Join {
left,
right,
on,
join_type,
algo_hint,
} => LogicalPlan::Join {
left: Box::new(cse_plan(*left, cse_counter)),
right: Box::new(cse_plan(*right, cse_counter)),
on,
join_type,
algo_hint,
},
LogicalPlan::SetOp {
op,
all,
left,
right,
} => LogicalPlan::SetOp {
op,
all,
left: Box::new(cse_plan(*left, cse_counter)),
right: Box::new(cse_plan(*right, cse_counter)),
},
other => other,
}
}
fn cse_predicate(pred: &str, counter: &mut usize) -> (String, Vec<(String, String)>) {
let Some(e) = parse_predicate(pred) else {
return (pred.to_string(), vec![]);
};
let common = find_common_subexprs(&e);
if common.is_empty() {
return (pred.to_string(), vec![]);
}
let mut bindings: Vec<(String, String)> = Vec::new();
let mut modified = e;
for (hash, expr_str, _count) in common {
let alias = format!("__cse_{}", *counter);
*counter += 1;
modified = replace_by_hash(modified, hash, &alias);
bindings.push((alias, expr_str));
}
(render(&modified), bindings)
}
fn replace_by_hash(e: Expr, target_hash: u64, alias: &str) -> Expr {
let is_nontrivial = !matches!(
e,
Expr::Identifier(_) | Expr::CompoundIdentifier(_) | Expr::Value(_)
);
if is_nontrivial && canonical_hash(&e) == target_hash {
return Expr::Identifier(make_ident(alias));
}
match e {
Expr::BinaryOp { left, op, right } => Expr::BinaryOp {
left: Box::new(replace_by_hash(*left, target_hash, alias)),
op,
right: Box::new(replace_by_hash(*right, target_hash, alias)),
},
Expr::UnaryOp { op, expr } => Expr::UnaryOp {
op,
expr: Box::new(replace_by_hash(*expr, target_hash, alias)),
},
Expr::Nested(inner) => Expr::Nested(Box::new(replace_by_hash(*inner, target_hash, alias))),
Expr::Between {
expr,
negated,
low,
high,
} => Expr::Between {
expr: Box::new(replace_by_hash(*expr, target_hash, alias)),
negated,
low: Box::new(replace_by_hash(*low, target_hash, alias)),
high: Box::new(replace_by_hash(*high, target_hash, alias)),
},
Expr::InList {
expr,
list,
negated,
} => Expr::InList {
expr: Box::new(replace_by_hash(*expr, target_hash, alias)),
list: list
.into_iter()
.map(|x| replace_by_hash(x, target_hash, alias))
.collect(),
negated,
},
other => other,
}
}
fn make_ident(name: &str) -> Ident {
Ident {
value: name.to_string(),
quote_style: None,
span: Span::empty(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::LogicalPlan;
fn scan(table: &str) -> LogicalPlan {
LogicalPlan::Scan {
table: table.to_string(),
alias: None,
limit: None,
}
}
#[test]
fn test_no_cse_needed() {
let plan = LogicalPlan::Filter {
input: Box::new(scan("t")),
predicate: "x > 1 AND y < 2".to_string(),
};
let result = CommonSubexprElimination.apply(plan);
assert!(
!matches!(result, LogicalPlan::Compute { .. }),
"no CSE expected for distinct subexpressions"
);
}
#[test]
fn test_cse_produces_compute_node() {
let plan = LogicalPlan::Filter {
input: Box::new(scan("t")),
predicate: "(x + 1) > 0 AND (x + 1) < 10".to_string(),
};
let result = CommonSubexprElimination.apply(plan);
match result {
LogicalPlan::Compute { bindings, input } => {
assert!(!bindings.is_empty(), "expected at least one binding");
assert!(
bindings[0].0.starts_with("__cse_"),
"alias should be __cse_N"
);
assert!(
matches!(*input, LogicalPlan::Filter { .. }),
"Compute should wrap a Filter"
);
}
other => panic!("expected Compute, got {other:?}"),
}
}
#[test]
fn test_no_cse_single_occurrence() {
let plan = LogicalPlan::Filter {
input: Box::new(scan("t")),
predicate: "(x + 1) > 0 AND y > 2".to_string(),
};
let result = CommonSubexprElimination.apply(plan);
assert!(
!matches!(result, LogicalPlan::Compute { .. }),
"no CSE for single occurrences"
);
}
}