Skip to main content

oxisql_parse/optimizer/
pushdown.rs

1//! Pass 1: Predicate pushdown.
2//!
3//! Push `Filter` nodes as close to `Scan` leaves as possible.  A filter
4//! sitting directly above an `INNER` or `CROSS` join can be moved below the
5//! join onto whichever side owns all referenced columns.  Filters above
6//! projections are pushed through when all predicate column names are in scope
7//! below the projection.
8
9use crate::LogicalPlan;
10use sqlparser::dialect::GenericDialect;
11use sqlparser::parser::Parser;
12
13use super::OptPass;
14
15// ── Public struct ────────────────────────────────────────────────────────────
16
17/// Push `Filter` nodes toward leaf `Scan` nodes.
18pub struct PredicatePushdown;
19
20impl OptPass for PredicatePushdown {
21    fn name(&self) -> &'static str {
22        "PredicatePushdown"
23    }
24
25    fn apply(&self, plan: LogicalPlan) -> LogicalPlan {
26        push_predicate(plan)
27    }
28}
29
30// ── Implementation ───────────────────────────────────────────────────────────
31
32/// Recursive predicate-pushdown rewrite.
33pub(super) fn push_predicate(plan: LogicalPlan) -> LogicalPlan {
34    match plan {
35        LogicalPlan::Filter { input, predicate } => {
36            // Recurse into the input first so inner filters are also pushed.
37            let inner = push_predicate(*input);
38
39            match inner {
40                // Filter over a Join — try to push into one side.
41                // NOTE: Only Inner and Cross joins preserve the semantics of
42                // pushing a single-side filter below the join.  For outer joins
43                // the non-preserved side must not be filtered before the join or
44                // we would lose rows that should appear as NULLs.
45                LogicalPlan::Join {
46                    left,
47                    right,
48                    on,
49                    join_type,
50                    ..
51                } => {
52                    let can_push =
53                        matches!(join_type, crate::JoinType::Inner | crate::JoinType::Cross);
54
55                    let left_tables = collect_table_names(&left);
56                    let right_tables = collect_table_names(&right);
57
58                    let only_left =
59                        can_push && predicate_refs_only(&predicate, &left_tables, &right_tables);
60                    let only_right =
61                        can_push && predicate_refs_only(&predicate, &right_tables, &left_tables);
62
63                    if only_left {
64                        let new_left = push_predicate(LogicalPlan::Filter {
65                            input: left,
66                            predicate,
67                        });
68                        LogicalPlan::Join {
69                            left: Box::new(new_left),
70                            right,
71                            on,
72                            join_type,
73                            algo_hint: None,
74                        }
75                    } else if only_right {
76                        let new_right = push_predicate(LogicalPlan::Filter {
77                            input: right,
78                            predicate,
79                        });
80                        LogicalPlan::Join {
81                            left,
82                            right: Box::new(new_right),
83                            on,
84                            join_type,
85                            algo_hint: None,
86                        }
87                    } else {
88                        // Cannot push — leave filter above the join.
89                        LogicalPlan::Filter {
90                            input: Box::new(LogicalPlan::Join {
91                                left,
92                                right,
93                                on,
94                                join_type,
95                                algo_hint: None,
96                            }),
97                            predicate,
98                        }
99                    }
100                }
101
102                // Filter over a Project — push through if all predicate columns
103                // are reachable below the projection.
104                LogicalPlan::Project { input, columns } => {
105                    let pred_cols = extract_identifier_names(&predicate);
106                    let proj_cols: Vec<&str> = columns.iter().map(|c| c.as_str()).collect();
107                    let all_in_scope = pred_cols
108                        .iter()
109                        .all(|pc| proj_cols.iter().any(|prc| prc.contains(pc.as_str())));
110
111                    if all_in_scope {
112                        LogicalPlan::Project {
113                            input: Box::new(push_predicate(LogicalPlan::Filter {
114                                input,
115                                predicate,
116                            })),
117                            columns,
118                        }
119                    } else {
120                        LogicalPlan::Filter {
121                            input: Box::new(LogicalPlan::Project { input, columns }),
122                            predicate,
123                        }
124                    }
125                }
126
127                // No further push opportunity — keep filter where it is.
128                other => LogicalPlan::Filter {
129                    input: Box::new(other),
130                    predicate,
131                },
132            }
133        }
134
135        // Recursively apply to all child plans.
136        LogicalPlan::Project { input, columns } => LogicalPlan::Project {
137            input: Box::new(push_predicate(*input)),
138            columns,
139        },
140        LogicalPlan::Join {
141            left,
142            right,
143            on,
144            join_type,
145            algo_hint,
146        } => LogicalPlan::Join {
147            left: Box::new(push_predicate(*left)),
148            right: Box::new(push_predicate(*right)),
149            on,
150            join_type,
151            algo_hint,
152        },
153        LogicalPlan::Aggregate {
154            input,
155            group_by,
156            aggregates,
157        } => LogicalPlan::Aggregate {
158            input: Box::new(push_predicate(*input)),
159            group_by,
160            aggregates,
161        },
162        LogicalPlan::Sort { input, order_by } => LogicalPlan::Sort {
163            input: Box::new(push_predicate(*input)),
164            order_by,
165        },
166        LogicalPlan::Limit {
167            input,
168            count,
169            offset,
170        } => LogicalPlan::Limit {
171            input: Box::new(push_predicate(*input)),
172            count,
173            offset,
174        },
175        // Leaf / terminal nodes: nothing to recurse into.
176        other => other,
177    }
178}
179
180// ── Helpers ───────────────────────────────────────────────────────────────────
181
182/// Collect all table names and aliases reachable from `plan`.
183fn collect_table_names(plan: &LogicalPlan) -> Vec<String> {
184    let mut names = Vec::new();
185    collect_table_names_inner(plan, &mut names);
186    names
187}
188
189fn collect_table_names_inner(plan: &LogicalPlan, out: &mut Vec<String>) {
190    match plan {
191        LogicalPlan::Scan { table, alias, .. } => {
192            out.push(table.clone());
193            if let Some(a) = alias {
194                out.push(a.clone());
195            }
196        }
197        LogicalPlan::Filter { input, .. }
198        | LogicalPlan::Project { input, .. }
199        | LogicalPlan::Aggregate { input, .. }
200        | LogicalPlan::Sort { input, .. }
201        | LogicalPlan::Limit { input, .. } => collect_table_names_inner(input, out),
202        LogicalPlan::Join { left, right, .. } => {
203            collect_table_names_inner(left, out);
204            collect_table_names_inner(right, out);
205        }
206        LogicalPlan::SetOp { left, right, .. } => {
207            collect_table_names_inner(left, out);
208            collect_table_names_inner(right, out);
209        }
210        LogicalPlan::Cte { query, .. } => collect_table_names_inner(query, out),
211        LogicalPlan::Window { input, .. } => collect_table_names_inner(input, out),
212        LogicalPlan::Subquery { query, .. }
213        | LogicalPlan::Exists {
214            subquery: query, ..
215        }
216        | LogicalPlan::InSubquery {
217            subquery: query, ..
218        } => {
219            collect_table_names_inner(query, out);
220        }
221        LogicalPlan::Compute { input, .. } => collect_table_names_inner(input, out),
222        LogicalPlan::CteRef { .. } | LogicalPlan::Values { .. } | LogicalPlan::Empty => {}
223    }
224}
225
226/// Return `true` when the predicate references at least one name from `owns`
227/// and no name from `excludes`.
228fn predicate_refs_only(predicate: &str, owns: &[String], excludes: &[String]) -> bool {
229    let refs_any_exclude = excludes.iter().any(|name| {
230        predicate.contains(&format!("{name}.")) || predicate_has_bare_word(predicate, name)
231    });
232    if refs_any_exclude {
233        return false;
234    }
235    owns.iter().any(|name| {
236        predicate.contains(&format!("{name}.")) || predicate_has_bare_word(predicate, name)
237    })
238}
239
240/// Check whether `word` appears as a token boundary in `text`.
241fn predicate_has_bare_word(text: &str, word: &str) -> bool {
242    if word.is_empty() {
243        return false;
244    }
245    let bytes = text.as_bytes();
246    let word_bytes = word.as_bytes();
247    let wlen = word_bytes.len();
248    let mut start = 0;
249    while start + wlen <= bytes.len() {
250        if &bytes[start..start + wlen] == word_bytes {
251            let before_ok = start == 0
252                || !bytes
253                    .get(start - 1)
254                    .map(|b| b.is_ascii_alphanumeric() || *b == b'_')
255                    .unwrap_or(false);
256            let after_ok = (start + wlen) >= bytes.len()
257                || !bytes
258                    .get(start + wlen)
259                    .map(|b| b.is_ascii_alphanumeric() || *b == b'_')
260                    .unwrap_or(false);
261            if before_ok && after_ok {
262                return true;
263            }
264        }
265        start += 1;
266    }
267    false
268}
269
270/// Extract simple identifier names from a predicate string via the sqlparser.
271fn extract_identifier_names(predicate: &str) -> Vec<String> {
272    let dialect = GenericDialect {};
273    let result = Parser::new(&dialect)
274        .try_with_sql(predicate)
275        .ok()
276        .and_then(|mut p| p.parse_expr().ok());
277
278    let mut names = Vec::new();
279    if let Some(expr) = result {
280        collect_identifiers_from_expr(&expr, &mut names);
281    }
282    names
283}
284
285fn collect_identifiers_from_expr(expr: &sqlparser::ast::Expr, out: &mut Vec<String>) {
286    use sqlparser::ast::Expr;
287    match expr {
288        Expr::Identifier(id) => out.push(id.value.clone()),
289        Expr::CompoundIdentifier(parts) => {
290            for part in parts {
291                out.push(part.value.clone());
292            }
293        }
294        Expr::BinaryOp { left, right, .. } => {
295            collect_identifiers_from_expr(left, out);
296            collect_identifiers_from_expr(right, out);
297        }
298        Expr::UnaryOp { expr, .. } => collect_identifiers_from_expr(expr, out),
299        Expr::Nested(inner) => collect_identifiers_from_expr(inner, out),
300        Expr::Between {
301            expr, low, high, ..
302        } => {
303            collect_identifiers_from_expr(expr, out);
304            collect_identifiers_from_expr(low, out);
305            collect_identifiers_from_expr(high, out);
306        }
307        Expr::InList { expr, list, .. } => {
308            collect_identifiers_from_expr(expr, out);
309            for item in list {
310                collect_identifiers_from_expr(item, out);
311            }
312        }
313        _ => {}
314    }
315}