Skip to main content

oxisql_parse/
decorrelate.rs

1//! Decorrelation of correlated subqueries into joins.
2//!
3//! Rewrites correlated `EXISTS` / `NOT EXISTS` → `LeftSemi` / `LeftAnti` joins
4//! and correlated `IN` → `LeftSemi` joins by pulling the correlation predicates
5//! out of the inner subquery's `WHERE` clause and using them as the join `ON`
6//! condition.
7
8use oxisql_core::OxiSqlError;
9use sqlparser::ast::Expr;
10
11use crate::optimizer::expr_util::{collect_colrefs, join_conjuncts, render, split_conjuncts};
12use crate::plan::{JoinType, LogicalPlan};
13
14// ── PlannerOptions ────────────────────────────────────────────────────────────
15
16/// Options that influence how `plan_query_with` builds the logical plan.
17#[derive(Debug, Clone)]
18pub struct PlannerOptions {
19    /// When `true`, correlated `EXISTS` / `NOT EXISTS` / `IN` subqueries are
20    /// rewritten into `LeftSemi` / `LeftAnti` joins at plan time.
21    pub decorrelate: bool,
22}
23
24impl Default for PlannerOptions {
25    fn default() -> Self {
26        Self { decorrelate: true }
27    }
28}
29
30// ── Public entry point ────────────────────────────────────────────────────────
31
32/// Post-facto decorrelation pass over an already-built `LogicalPlan`.
33///
34/// For `Filter` nodes whose predicate contains correlated `EXISTS` or
35/// `InSubquery` conjuncts, the conjuncts are replaced by `LeftSemi` /
36/// `LeftAnti` joins against the inner plan.  Uncorrelated subqueries and
37/// structural `Exists` / `InSubquery` plan nodes without an accessible outer
38/// plan are left unchanged.
39///
40/// `outer_scope` carries the table/alias names that are in scope from an
41/// enclosing query block; pass `&[]` at the top level.
42pub fn decorrelate_plan(plan: LogicalPlan, outer_scope: &[String]) -> LogicalPlan {
43    match plan {
44        LogicalPlan::Filter { input, predicate } => {
45            let input = decorrelate_plan(*input, outer_scope);
46            // Collect outer scope from the (already-decorrelated) input plan.
47            let mut scope: Vec<String> = collect_plan_table_names(&input);
48            scope.extend_from_slice(outer_scope);
49
50            match apply_decorrelated_filter(input, &predicate, &scope) {
51                Ok(new_plan) => new_plan,
52                Err(_) => LogicalPlan::Filter {
53                    input: Box::new(decorrelate_plan(
54                        LogicalPlan::Scan {
55                            table: "__err__".to_string(),
56                            alias: None,
57                            limit: None,
58                        },
59                        outer_scope,
60                    )),
61                    predicate,
62                },
63            }
64        }
65
66        // Recursively decorrelate children of all other variants.
67        LogicalPlan::Project { input, columns } => LogicalPlan::Project {
68            input: Box::new(decorrelate_plan(*input, outer_scope)),
69            columns,
70        },
71        LogicalPlan::Aggregate {
72            input,
73            group_by,
74            aggregates,
75        } => LogicalPlan::Aggregate {
76            input: Box::new(decorrelate_plan(*input, outer_scope)),
77            group_by,
78            aggregates,
79        },
80        LogicalPlan::Sort { input, order_by } => LogicalPlan::Sort {
81            input: Box::new(decorrelate_plan(*input, outer_scope)),
82            order_by,
83        },
84        LogicalPlan::Limit {
85            input,
86            count,
87            offset,
88        } => LogicalPlan::Limit {
89            input: Box::new(decorrelate_plan(*input, outer_scope)),
90            count,
91            offset,
92        },
93        LogicalPlan::Window { input, functions } => LogicalPlan::Window {
94            input: Box::new(decorrelate_plan(*input, outer_scope)),
95            functions,
96        },
97        LogicalPlan::Compute { input, bindings } => LogicalPlan::Compute {
98            input: Box::new(decorrelate_plan(*input, outer_scope)),
99            bindings,
100        },
101        LogicalPlan::Join {
102            left,
103            right,
104            on,
105            join_type,
106            algo_hint,
107        } => LogicalPlan::Join {
108            left: Box::new(decorrelate_plan(*left, outer_scope)),
109            right: Box::new(decorrelate_plan(*right, outer_scope)),
110            on,
111            join_type,
112            algo_hint,
113        },
114        LogicalPlan::SetOp {
115            op,
116            all,
117            left,
118            right,
119        } => LogicalPlan::SetOp {
120            op,
121            all,
122            left: Box::new(decorrelate_plan(*left, outer_scope)),
123            right: Box::new(decorrelate_plan(*right, outer_scope)),
124        },
125        LogicalPlan::Cte {
126            name,
127            query,
128            recursive,
129        } => LogicalPlan::Cte {
130            name,
131            query: Box::new(decorrelate_plan(*query, outer_scope)),
132            recursive,
133        },
134        LogicalPlan::Subquery { query, alias } => LogicalPlan::Subquery {
135            query: Box::new(decorrelate_plan(*query, outer_scope)),
136            alias,
137        },
138        LogicalPlan::Exists { subquery, negated } => LogicalPlan::Exists {
139            subquery: Box::new(decorrelate_plan(*subquery, outer_scope)),
140            negated,
141        },
142        LogicalPlan::InSubquery {
143            expr,
144            subquery,
145            negated,
146        } => LogicalPlan::InSubquery {
147            expr,
148            subquery: Box::new(decorrelate_plan(*subquery, outer_scope)),
149            negated,
150        },
151        // Leaves
152        other => other,
153    }
154}
155
156/// Try to rewrite correlated EXISTS / IN conjuncts in `predicate` into
157/// LeftSemi / LeftAnti joins on top of `outer_plan`.
158///
159/// Returns the (possibly modified) plan with the decorrelated subqueries
160/// turned into joins and any remaining regular predicates wrapped in a Filter.
161fn apply_decorrelated_filter(
162    outer_plan: LogicalPlan,
163    predicate: &str,
164    outer_scope: &[String],
165) -> Result<LogicalPlan, OxiSqlError> {
166    let Some(pred_expr) = crate::parse_predicate(predicate) else {
167        // Can't parse; keep as-is.
168        return Ok(LogicalPlan::Filter {
169            input: Box::new(outer_plan),
170            predicate: predicate.to_string(),
171        });
172    };
173
174    let conjuncts = split_conjuncts(pred_expr);
175    let mut plan = outer_plan;
176    let mut remaining: Vec<Expr> = Vec::new();
177
178    for conjunct in conjuncts {
179        match conjunct {
180            Expr::Exists {
181                ref subquery,
182                negated,
183            } => {
184                let inner = crate::planner::plan_query(subquery)?;
185                let (non_corr_plan, corr_preds) = extract_correlated_filter(inner, outer_scope);
186                if corr_preds.is_empty() {
187                    // Uncorrelated — keep as a structural filter expression.
188                    remaining.push(conjunct);
189                } else {
190                    let join_on = corr_preds.join(" AND ");
191                    let join_type = if negated {
192                        JoinType::LeftAnti
193                    } else {
194                        JoinType::LeftSemi
195                    };
196                    plan = LogicalPlan::Join {
197                        left: Box::new(plan),
198                        right: Box::new(non_corr_plan),
199                        on: join_on,
200                        join_type,
201                        algo_hint: None,
202                    };
203                }
204            }
205
206            Expr::InSubquery {
207                ref expr,
208                ref subquery,
209                negated,
210            } => {
211                let inner = crate::planner::plan_query(subquery)?;
212                let proj_cols = extract_projection_cols(&inner);
213                let (non_corr_plan, mut corr_preds) = extract_correlated_filter(inner, outer_scope);
214
215                if corr_preds.is_empty() {
216                    // Uncorrelated.
217                    remaining.push(conjunct);
218                } else {
219                    let expr_str = expr.to_string();
220                    // Prepend the equality between the outer expr and the inner projected column.
221                    if let Some(proj_col) = proj_cols.first() {
222                        corr_preds.insert(0, format!("{expr_str} = {proj_col}"));
223                    }
224                    let join_on = corr_preds.join(" AND ");
225                    let join_type = if negated {
226                        JoinType::LeftAnti
227                    } else {
228                        JoinType::LeftSemi
229                    };
230                    plan = LogicalPlan::Join {
231                        left: Box::new(plan),
232                        right: Box::new(non_corr_plan),
233                        on: join_on,
234                        join_type,
235                        algo_hint: None,
236                    };
237                }
238            }
239
240            other => remaining.push(other),
241        }
242    }
243
244    // Wrap with a Filter for any remaining (non-decorrelatable) conjuncts.
245    if let Some(e) = join_conjuncts(remaining) {
246        plan = LogicalPlan::Filter {
247            input: Box::new(plan),
248            predicate: render(&e),
249        };
250    }
251
252    Ok(plan)
253}
254
255// ── Internals ─────────────────────────────────────────────────────────────────
256
257/// Entry point called from `plan_select_with_opts` in `planner.rs`.
258///
259/// Applies decorrelation to the WHERE expression `pred` given the outer
260/// table scope `outer_scope`.
261pub(crate) fn apply_decorrelated_where(
262    outer_plan: LogicalPlan,
263    pred: &Expr,
264    outer_scope: &[String],
265) -> Result<LogicalPlan, OxiSqlError> {
266    let conjuncts = split_conjuncts(pred.clone());
267    let mut plan = outer_plan;
268    let mut remaining: Vec<Expr> = Vec::new();
269
270    for conjunct in conjuncts {
271        match conjunct {
272            Expr::Exists {
273                ref subquery,
274                negated,
275            } => {
276                let opts_no_decorr = PlannerOptions { decorrelate: false };
277                let inner = crate::planner::plan_query_with_opts(subquery, &opts_no_decorr)?;
278                let (non_corr_plan, corr_preds) = extract_correlated_filter(inner, outer_scope);
279
280                if corr_preds.is_empty() {
281                    remaining.push(conjunct);
282                } else {
283                    let join_on = corr_preds.join(" AND ");
284                    let join_type = if negated {
285                        JoinType::LeftAnti
286                    } else {
287                        JoinType::LeftSemi
288                    };
289                    plan = LogicalPlan::Join {
290                        left: Box::new(plan),
291                        right: Box::new(non_corr_plan),
292                        on: join_on,
293                        join_type,
294                        algo_hint: None,
295                    };
296                }
297            }
298
299            Expr::InSubquery {
300                ref expr,
301                ref subquery,
302                negated,
303            } => {
304                let opts_no_decorr = PlannerOptions { decorrelate: false };
305                let inner = crate::planner::plan_query_with_opts(subquery, &opts_no_decorr)?;
306                let proj_cols = extract_projection_cols(&inner);
307                let (non_corr_plan, mut corr_preds) = extract_correlated_filter(inner, outer_scope);
308
309                if corr_preds.is_empty() {
310                    remaining.push(conjunct);
311                } else {
312                    let expr_str = expr.to_string();
313                    if let Some(proj_col) = proj_cols.first() {
314                        corr_preds.insert(0, format!("{expr_str} = {proj_col}"));
315                    }
316                    let join_on = corr_preds.join(" AND ");
317                    let join_type = if negated {
318                        JoinType::LeftAnti
319                    } else {
320                        JoinType::LeftSemi
321                    };
322                    plan = LogicalPlan::Join {
323                        left: Box::new(plan),
324                        right: Box::new(non_corr_plan),
325                        on: join_on,
326                        join_type,
327                        algo_hint: None,
328                    };
329                }
330            }
331
332            other => remaining.push(other),
333        }
334    }
335
336    if let Some(e) = join_conjuncts(remaining) {
337        plan = LogicalPlan::Filter {
338            input: Box::new(plan),
339            predicate: render(&e),
340        };
341    }
342
343    Ok(plan)
344}
345
346/// Split a filter's predicate into correlated and non-correlated conjuncts.
347///
348/// Returns `(modified_plan_without_corr_predicates, correlated_conjuncts_as_strings)`.
349/// Recursion descends through `Project` to find the first `Filter`.
350fn extract_correlated_filter(
351    plan: LogicalPlan,
352    outer_scope: &[String],
353) -> (LogicalPlan, Vec<String>) {
354    match plan {
355        LogicalPlan::Filter { input, predicate } => {
356            let Some(e) = crate::parse_predicate(&predicate) else {
357                return (LogicalPlan::Filter { input, predicate }, vec![]);
358            };
359            let conjuncts = split_conjuncts(e);
360            let mut corr: Vec<String> = Vec::new();
361            let mut non_corr: Vec<Expr> = Vec::new();
362
363            for c in conjuncts {
364                let refs = collect_colrefs(&c);
365                let is_corr = refs.iter().any(|r| {
366                    r.qualifier
367                        .as_deref()
368                        .map(|q| outer_scope.iter().any(|s| s == q))
369                        .unwrap_or(false)
370                });
371                if is_corr {
372                    corr.push(render(&c));
373                } else {
374                    non_corr.push(c);
375                }
376            }
377
378            let new_plan = if let Some(e) = join_conjuncts(non_corr) {
379                LogicalPlan::Filter {
380                    input,
381                    predicate: render(&e),
382                }
383            } else {
384                *input
385            };
386            (new_plan, corr)
387        }
388
389        LogicalPlan::Project { input, columns } => {
390            let (new_input, corr) = extract_correlated_filter(*input, outer_scope);
391            (
392                LogicalPlan::Project {
393                    input: Box::new(new_input),
394                    columns,
395                },
396                corr,
397            )
398        }
399
400        // For other variants, no correlation found at this level.
401        other => (other, vec![]),
402    }
403}
404
405/// Extract projected column expressions from the top-level of a plan.
406fn extract_projection_cols(plan: &LogicalPlan) -> Vec<String> {
407    match plan {
408        LogicalPlan::Project { columns, .. } => columns.clone(),
409        LogicalPlan::Filter { input, .. } => extract_projection_cols(input),
410        _ => vec![],
411    }
412}
413
414/// Collect all table names and aliases reachable from `plan`.
415fn collect_plan_table_names(plan: &LogicalPlan) -> Vec<String> {
416    let mut out = Vec::new();
417    collect_names_inner(plan, &mut out);
418    out
419}
420
421fn collect_names_inner(plan: &LogicalPlan, out: &mut Vec<String>) {
422    match plan {
423        LogicalPlan::Scan { table, alias, .. } => {
424            out.push(table.clone());
425            if let Some(a) = alias {
426                out.push(a.clone());
427            }
428        }
429        LogicalPlan::Filter { input, .. }
430        | LogicalPlan::Project { input, .. }
431        | LogicalPlan::Aggregate { input, .. }
432        | LogicalPlan::Sort { input, .. }
433        | LogicalPlan::Limit { input, .. }
434        | LogicalPlan::Window { input, .. }
435        | LogicalPlan::Compute { input, .. } => collect_names_inner(input, out),
436        LogicalPlan::Join { left, right, .. } | LogicalPlan::SetOp { left, right, .. } => {
437            collect_names_inner(left, out);
438            collect_names_inner(right, out);
439        }
440        _ => {}
441    }
442}