pub mod cse;
pub mod expr_util;
mod folding;
pub mod join_algo;
pub mod join_reorder;
mod limit;
mod pruning;
mod pushdown;
pub mod simplify;
pub use cse::CommonSubexprElimination;
pub use folding::ConstantFolding;
pub use join_algo::{JoinAlgoHint, JoinAlgorithmPass};
pub use limit::LimitPushThrough;
pub use pruning::ProjectionPruning;
pub use pushdown::PredicatePushdown;
pub use simplify::PredicateSimplification;
use crate::LogicalPlan;
pub trait OptPass: Send + Sync {
fn name(&self) -> &'static str;
fn apply(&self, plan: LogicalPlan) -> LogicalPlan;
}
pub struct Optimizer {
passes: Vec<Box<dyn OptPass>>,
}
impl Optimizer {
pub fn new() -> Self {
let passes: Vec<Box<dyn OptPass>> = vec![
Box::new(PredicatePushdown),
Box::new(PredicateSimplification),
Box::new(CommonSubexprElimination),
Box::new(ProjectionPruning { required: vec![] }),
Box::new(ConstantFolding),
Box::new(LimitPushThrough),
Box::new(JoinAlgorithmPass),
];
Self { passes }
}
pub fn with_passes(passes: Vec<Box<dyn OptPass>>) -> Self {
Self { passes }
}
pub fn optimize(&self, plan: LogicalPlan) -> LogicalPlan {
self.passes.iter().fold(plan, |p, pass| pass.apply(p))
}
}
impl Default for Optimizer {
fn default() -> Self {
Self::new()
}
}
impl Optimizer {
pub fn with_cost_model(model: crate::cost::CostModel) -> Self {
use std::sync::Arc;
let model = Arc::new(model);
let mut opt = Self::new();
opt.passes
.push(Box::new(crate::optimizer::join_reorder::JoinReorder::new(
model,
)));
opt
}
}
pub fn optimize(plan: LogicalPlan) -> LogicalPlan {
Optimizer::new().optimize(plan)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{JoinType, LogicalPlan};
fn scan(table: &str) -> LogicalPlan {
LogicalPlan::Scan {
table: table.to_string(),
alias: None,
limit: None,
}
}
fn scan_alias(table: &str, alias: &str) -> LogicalPlan {
LogicalPlan::Scan {
table: table.to_string(),
alias: Some(alias.to_string()),
limit: None,
}
}
fn filter(input: LogicalPlan, pred: &str) -> LogicalPlan {
LogicalPlan::Filter {
input: Box::new(input),
predicate: pred.to_string(),
}
}
fn join(left: LogicalPlan, right: LogicalPlan, on: &str) -> LogicalPlan {
LogicalPlan::Join {
left: Box::new(left),
right: Box::new(right),
on: on.to_string(),
join_type: JoinType::Inner,
algo_hint: None,
}
}
#[test]
fn test_predicate_pushdown_moves_filter_below_join() {
let plan = filter(
join(scan_alias("a", "a"), scan_alias("b", "b"), "a.id = b.id"),
"a.x > 1",
);
let result = PredicatePushdown.apply(plan);
match result {
LogicalPlan::Join { left, .. } => match *left {
LogicalPlan::Filter { predicate, .. } => {
assert_eq!(predicate, "a.x > 1");
}
other => panic!("expected Filter on left side, got {other:?}"),
},
other => panic!("expected Join at root after pushdown, got {other:?}"),
}
}
#[test]
fn test_projection_pruning_reduces_columns() {
let plan = LogicalPlan::Project {
input: Box::new(scan("t")),
columns: vec!["col1".to_string(), "col2".to_string(), "col3".to_string()],
};
let pass = ProjectionPruning {
required: vec!["col1".to_string()],
};
let result = pass.apply(plan);
match result {
LogicalPlan::Project { columns, .. } => {
assert_eq!(columns, vec!["col1".to_string()]);
}
other => panic!("expected Project, got {other:?}"),
}
}
#[test]
fn test_constant_folding_true_removes_filter() {
let plan = filter(scan("t"), "1 = 1");
let result = ConstantFolding.apply(plan);
match result {
LogicalPlan::Scan { table, .. } => assert_eq!(table, "t"),
other => panic!("expected Scan after folding 1=1, got {other:?}"),
}
}
#[test]
fn test_constant_folding_false_yields_empty() {
let plan = filter(scan("t"), "1 = 2");
let result = ConstantFolding.apply(plan);
assert!(
matches!(result, LogicalPlan::Empty),
"expected Empty after folding 1=2, got {result:?}"
);
}
#[test]
fn test_limit_push_through_scan() {
let plan = LogicalPlan::Limit {
input: Box::new(scan("t")),
count: Some(10),
offset: None,
};
let result = LimitPushThrough.apply(plan);
match result {
LogicalPlan::Scan { limit, .. } => assert_eq!(limit, Some(10)),
other => panic!("expected Scan with limit after push-through, got {other:?}"),
}
}
#[test]
fn test_optimizer_pipeline_composes() {
let base = filter(
join(scan_alias("a", "a"), scan_alias("b", "b"), "a.id = b.id"),
"a.id > 0",
);
let limited = LogicalPlan::Limit {
input: Box::new(base),
count: Some(5),
offset: None,
};
let result = Optimizer::new().optimize(limited);
let text = crate::explain(&result);
assert!(!text.is_empty(), "explain should produce non-empty output");
assert!(
text.contains("Join") || text.contains("Scan"),
"expected join or scan in plan: {text}"
);
}
}