Skip to main content

oxisql_parse/optimizer/
pruning.rs

1//! Pass 2: Projection pruning.
2//!
3//! Given a required column set propagated downward from the top of the plan,
4//! trim each `Project` node's column list to only the columns in the required
5//! set.  When `required` is empty, this pass is a no-op.
6
7use crate::LogicalPlan;
8
9use super::OptPass;
10
11// ── Public struct ─────────────────────────────────────────────────────────────
12
13/// Prune `Project` column lists to only those columns required by the parent.
14///
15/// When `required` is empty, this pass is a no-op (all columns are kept).
16pub struct ProjectionPruning {
17    /// Column names (bare names, not qualified) needed by the consumer of this
18    /// plan.  An empty list means "all columns required" (no pruning).
19    pub required: Vec<String>,
20}
21
22impl OptPass for ProjectionPruning {
23    fn name(&self) -> &'static str {
24        "ProjectionPruning"
25    }
26
27    fn apply(&self, plan: LogicalPlan) -> LogicalPlan {
28        if self.required.is_empty() {
29            return plan;
30        }
31        prune_projections(plan, &self.required)
32    }
33}
34
35// ── Implementation ────────────────────────────────────────────────────────────
36
37pub(super) fn prune_projections(plan: LogicalPlan, required: &[String]) -> LogicalPlan {
38    match plan {
39        LogicalPlan::Project { input, columns } => {
40            // Keep only columns that appear in the required set.
41            let pruned: Vec<String> = columns
42                .into_iter()
43                .filter(|col| {
44                    required
45                        .iter()
46                        .any(|r| col == r || col.contains(r.as_str()))
47                })
48                .collect();
49
50            // Propagate the (possibly pruned) requirement downward.
51            let new_required = if pruned.is_empty() {
52                required.to_vec()
53            } else {
54                pruned.clone()
55            };
56            let final_cols = if pruned.is_empty() {
57                required.to_vec()
58            } else {
59                pruned
60            };
61
62            LogicalPlan::Project {
63                input: Box::new(prune_projections(*input, &new_required)),
64                columns: final_cols,
65            }
66        }
67        LogicalPlan::Filter { input, predicate } => LogicalPlan::Filter {
68            input: Box::new(prune_projections(*input, required)),
69            predicate,
70        },
71        LogicalPlan::Aggregate {
72            input,
73            group_by,
74            aggregates,
75        } => LogicalPlan::Aggregate {
76            input: Box::new(prune_projections(*input, required)),
77            group_by,
78            aggregates,
79        },
80        LogicalPlan::Sort { input, order_by } => LogicalPlan::Sort {
81            input: Box::new(prune_projections(*input, required)),
82            order_by,
83        },
84        LogicalPlan::Limit {
85            input,
86            count,
87            offset,
88        } => LogicalPlan::Limit {
89            input: Box::new(prune_projections(*input, required)),
90            count,
91            offset,
92        },
93        LogicalPlan::Join {
94            left,
95            right,
96            on,
97            join_type,
98            algo_hint,
99        } => LogicalPlan::Join {
100            left: Box::new(prune_projections(*left, required)),
101            right: Box::new(prune_projections(*right, required)),
102            on,
103            join_type,
104            algo_hint,
105        },
106        other => other,
107    }
108}