Skip to main content

oxisql_parse/
planner.rs

1//! SQL statement planner: converts parsed AST statements to [`LogicalPlan`]s.
2
3use oxisql_core::OxiSqlError;
4use sqlparser::ast::Expr;
5
6use crate::plan::{JoinType, LogicalPlan, SortExpr, WindowFunctionDef};
7use crate::{agg, setops, window};
8
9// ── Planner functions ────────────────────────────────────────────────────────
10
11/// Transform a parsed [`sqlparser::ast::Statement`] into a [`LogicalPlan`].
12///
13/// Supports `SELECT` (via [`plan_query`]), `INSERT`, `UPDATE`, and `DELETE`.
14/// All other statement types return [`LogicalPlan::Empty`].
15///
16/// # Errors
17///
18/// Returns [`OxiSqlError::Parse`] if the statement structure is unexpected.
19pub fn plan_statement(stmt: &sqlparser::ast::Statement) -> Result<LogicalPlan, OxiSqlError> {
20    match stmt {
21        sqlparser::ast::Statement::Query(query) => plan_query(query),
22        sqlparser::ast::Statement::Insert(insert) => {
23            let columns: Vec<String> = insert
24                .columns
25                .iter()
26                .filter_map(|col| {
27                    col.0
28                        .last()
29                        .and_then(|p| p.as_ident())
30                        .map(|id| id.value.clone())
31                })
32                .collect();
33            let rows = insert
34                .source
35                .as_ref()
36                .and_then(|src| {
37                    if let sqlparser::ast::SetExpr::Values(ref vals) = *src.body {
38                        Some(vals.rows.len())
39                    } else {
40                        None
41                    }
42                })
43                .unwrap_or(0);
44            Ok(LogicalPlan::Values { columns, rows })
45        }
46        sqlparser::ast::Statement::Update(update) => {
47            let (table, alias) = table_factor_name_alias(&update.table.relation);
48            let base = LogicalPlan::Scan {
49                table,
50                alias,
51                limit: None,
52            };
53            Ok(match &update.selection {
54                Some(pred) => LogicalPlan::Filter {
55                    input: Box::new(base),
56                    predicate: pred.to_string(),
57                },
58                None => base,
59            })
60        }
61        sqlparser::ast::Statement::Delete(delete) => {
62            let base = match &delete.from {
63                sqlparser::ast::FromTable::WithFromKeyword(twjs) => build_scan_from_twjs(twjs),
64                sqlparser::ast::FromTable::WithoutKeyword(twjs) => build_scan_from_twjs(twjs),
65            };
66            Ok(match &delete.selection {
67                Some(pred) => LogicalPlan::Filter {
68                    input: Box::new(base),
69                    predicate: pred.to_string(),
70                },
71                None => base,
72            })
73        }
74        _ => Ok(LogicalPlan::Empty),
75    }
76}
77
78/// Build a [`LogicalPlan`] from a parsed `SELECT`/`UNION`/subquery.
79///
80/// # Errors
81///
82/// Returns [`OxiSqlError::Parse`] for unsupported `SetExpr` variants.
83pub fn plan_query(query: &sqlparser::ast::Query) -> Result<LogicalPlan, OxiSqlError> {
84    let mut plan = plan_set_expr(&query.body)?;
85
86    // ORDER BY
87    if let Some(ref order_by) = query.order_by {
88        if let sqlparser::ast::OrderByKind::Expressions(ref exprs) = order_by.kind {
89            if !exprs.is_empty() {
90                let sort_keys: Vec<SortExpr> = exprs
91                    .iter()
92                    .map(|e| SortExpr {
93                        column: e.expr.to_string(),
94                        ascending: e.options.asc.unwrap_or(true),
95                    })
96                    .collect();
97                plan = LogicalPlan::Sort {
98                    input: Box::new(plan),
99                    order_by: sort_keys,
100                };
101            }
102        }
103    }
104
105    // LIMIT / OFFSET
106    if let Some(ref limit_clause) = query.limit_clause {
107        let (count, offset) = extract_limit_offset(limit_clause);
108        if count.is_some() || offset.is_some() {
109            plan = LogicalPlan::Limit {
110                input: Box::new(plan),
111                count,
112                offset,
113            };
114        }
115    }
116
117    // WITH / CTE — wrap the already-built body plan in Cte nodes, one per CTE
118    // in declaration order (last CTE wraps the body, each prior one wraps the next).
119    if let Some(ref with) = query.with {
120        let recursive = with.recursive;
121        for cte in with.cte_tables.iter().rev() {
122            let cte_name = cte.alias.name.value.clone();
123            let inner_plan = plan_query(&cte.query)?;
124            plan = LogicalPlan::Cte {
125                name: cte_name,
126                query: Box::new(inner_plan),
127                recursive,
128            };
129        }
130    }
131
132    Ok(plan)
133}
134
135/// Walk a `SetExpr`, building the inner part of the plan (up to ORDER BY/LIMIT).
136pub(crate) fn plan_set_expr(body: &sqlparser::ast::SetExpr) -> Result<LogicalPlan, OxiSqlError> {
137    match body {
138        sqlparser::ast::SetExpr::Select(sel) => plan_select(sel),
139        sqlparser::ast::SetExpr::Query(inner) => plan_query(inner),
140        sqlparser::ast::SetExpr::SetOperation {
141            op,
142            set_quantifier,
143            left,
144            right,
145        } => setops::plan_set_operation(op, set_quantifier, left, right, &plan_set_expr),
146        _ => Ok(LogicalPlan::Empty),
147    }
148}
149
150/// Extract a scalar subquery plan from a SELECT projection item, or `None`.
151fn extract_subquery_from_select_item(
152    item: &sqlparser::ast::SelectItem,
153) -> Result<Option<LogicalPlan>, OxiSqlError> {
154    let expr = match item {
155        sqlparser::ast::SelectItem::UnnamedExpr(e) => e,
156        sqlparser::ast::SelectItem::ExprWithAlias { expr: e, .. } => e,
157        _ => return Ok(None),
158    };
159    if let Expr::Subquery(query) = expr {
160        Ok(Some(plan_query(query)?))
161    } else {
162        Ok(None)
163    }
164}
165
166/// Build the plan for a `SELECT` node.
167pub(crate) fn plan_select(sel: &sqlparser::ast::Select) -> Result<LogicalPlan, OxiSqlError> {
168    // 1 + 2. FROM + JOINs
169    let mut plan = build_from_plan(&sel.from);
170
171    // 3. WHERE — special-case Exists / InSubquery; fall through to Filter otherwise.
172    if let Some(ref pred) = sel.selection {
173        match pred {
174            Expr::Exists { subquery, negated } => {
175                let inner = plan_query(subquery)?;
176                plan = LogicalPlan::Exists {
177                    subquery: Box::new(inner),
178                    negated: *negated,
179                };
180            }
181            Expr::InSubquery {
182                expr,
183                subquery,
184                negated,
185            } => {
186                let inner = plan_query(subquery)?;
187                plan = LogicalPlan::InSubquery {
188                    expr: expr.to_string(),
189                    subquery: Box::new(inner),
190                    negated: *negated,
191                };
192            }
193            other => {
194                plan = LogicalPlan::Filter {
195                    input: Box::new(plan),
196                    predicate: other.to_string(),
197                };
198            }
199        }
200    }
201
202    // 4. GROUP BY / aggregates
203    let group_by_exprs: Vec<String> = match &sel.group_by {
204        sqlparser::ast::GroupByExpr::Expressions(exprs, _) => {
205            exprs.iter().map(|e| e.to_string()).collect()
206        }
207        sqlparser::ast::GroupByExpr::All(_) => vec!["*".to_string()],
208    };
209    let aggregates: Vec<String> = sel
210        .projection
211        .iter()
212        .filter(|item| agg::projection_item_is_aggregate(item))
213        .map(|item| item.to_string())
214        .collect();
215    if !group_by_exprs.is_empty() || !aggregates.is_empty() {
216        plan = LogicalPlan::Aggregate {
217            input: Box::new(plan),
218            group_by: group_by_exprs,
219            aggregates,
220        };
221        // HAVING — wrap the Aggregate in a Filter node.
222        if let Some(ref having_expr) = sel.having {
223            plan = LogicalPlan::Filter {
224                input: Box::new(plan),
225                predicate: having_expr.to_string(),
226            };
227        }
228    }
229
230    // 5. Projection — skip if pure `SELECT *`
231    let all_wildcard = sel.projection.iter().all(|item| {
232        matches!(
233            item,
234            sqlparser::ast::SelectItem::Wildcard(_)
235                | sqlparser::ast::SelectItem::QualifiedWildcard(_, _)
236        )
237    });
238    if !all_wildcard
239        && !matches!(plan, LogicalPlan::Aggregate { .. })
240        && !matches!(plan, LogicalPlan::Filter {
241            input: ref i, ..
242        } if matches!(i.as_ref(), LogicalPlan::Aggregate { .. }))
243    {
244        // Collect scalar subqueries from the projection.
245        let subquery_plans: Vec<LogicalPlan> = sel
246            .projection
247            .iter()
248            .filter_map(|item| extract_subquery_from_select_item(item).ok().flatten())
249            .collect();
250        if !subquery_plans.is_empty() {
251            for sub_plan in subquery_plans {
252                let alias = sel.projection.iter().find_map(|it| match it {
253                    sqlparser::ast::SelectItem::ExprWithAlias {
254                        expr: Expr::Subquery(_),
255                        alias,
256                    } => Some(alias.value.clone()),
257                    _ => None,
258                });
259                plan = LogicalPlan::Subquery {
260                    query: Box::new(sub_plan),
261                    alias,
262                };
263            }
264        } else {
265            let columns: Vec<String> = sel.projection.iter().map(|item| item.to_string()).collect();
266            plan = LogicalPlan::Project {
267                input: Box::new(plan),
268                columns,
269            };
270        }
271    }
272
273    // 6. Window functions — wrap in a Window node if any OVER clauses are present.
274    let window_defs: Vec<WindowFunctionDef> = sel
275        .projection
276        .iter()
277        .filter(|item| window::select_item_is_windowed(item))
278        .filter_map(window::extract_window_def)
279        .collect();
280    if !window_defs.is_empty() {
281        plan = LogicalPlan::Window {
282            input: Box::new(plan),
283            functions: window_defs,
284        };
285    }
286
287    Ok(plan)
288}
289
290/// Build a plan from the `FROM` clause (a list of `TableWithJoins`).
291///
292/// Multiple FROM items are treated as a cross-join chain.
293pub(crate) fn build_from_plan(from: &[sqlparser::ast::TableWithJoins]) -> LogicalPlan {
294    if from.is_empty() {
295        return LogicalPlan::Empty;
296    }
297
298    // Build first table's plan (including its explicit JOINs).
299    let mut plan = build_table_with_joins_plan(&from[0]);
300
301    // Additional FROM items → cross-join.
302    for twj in from.iter().skip(1) {
303        let right = build_table_with_joins_plan(twj);
304        plan = LogicalPlan::Join {
305            left: Box::new(plan),
306            right: Box::new(right),
307            on: String::new(),
308            join_type: JoinType::Cross,
309            algo_hint: None,
310        };
311    }
312
313    plan
314}
315
316/// Build a plan from one `TableWithJoins` entry (base relation + explicit joins).
317fn build_table_with_joins_plan(twj: &sqlparser::ast::TableWithJoins) -> LogicalPlan {
318    let (table, alias) = table_factor_name_alias(&twj.relation);
319    let mut plan = LogicalPlan::Scan {
320        table,
321        alias,
322        limit: None,
323    };
324
325    for join in &twj.joins {
326        let (jt, alias) = table_factor_name_alias(&join.relation);
327        let right = LogicalPlan::Scan {
328            table: jt,
329            alias,
330            limit: None,
331        };
332
333        let (join_type, on_expr) = join_operator_info(&join.join_operator);
334
335        plan = LogicalPlan::Join {
336            left: Box::new(plan),
337            right: Box::new(right),
338            on: on_expr,
339            join_type,
340            algo_hint: None,
341        };
342    }
343
344    plan
345}
346
347/// Extract table name and alias from a `TableFactor`.
348pub(crate) fn table_factor_name_alias(
349    factor: &sqlparser::ast::TableFactor,
350) -> (String, Option<String>) {
351    match factor {
352        sqlparser::ast::TableFactor::Table { name, alias, .. } => {
353            let tname = name.to_string();
354            let talias = alias.as_ref().map(|a| a.name.value.clone());
355            (tname, talias)
356        }
357        other => (other.to_string(), None),
358    }
359}
360
361/// Extract `JoinType` and ON expression string from a `JoinOperator`.
362fn join_operator_info(op: &sqlparser::ast::JoinOperator) -> (JoinType, String) {
363    let constraint_on = |c: &sqlparser::ast::JoinConstraint| -> String {
364        match c {
365            sqlparser::ast::JoinConstraint::On(expr) => expr.to_string(),
366            _ => String::new(),
367        }
368    };
369
370    match op {
371        sqlparser::ast::JoinOperator::Join(c)
372        | sqlparser::ast::JoinOperator::Inner(c)
373        | sqlparser::ast::JoinOperator::Semi(c)
374        | sqlparser::ast::JoinOperator::LeftSemi(c)
375        | sqlparser::ast::JoinOperator::RightSemi(c)
376        | sqlparser::ast::JoinOperator::Anti(c)
377        | sqlparser::ast::JoinOperator::LeftAnti(c)
378        | sqlparser::ast::JoinOperator::RightAnti(c)
379        | sqlparser::ast::JoinOperator::StraightJoin(c) => (JoinType::Inner, constraint_on(c)),
380        sqlparser::ast::JoinOperator::Left(c) | sqlparser::ast::JoinOperator::LeftOuter(c) => {
381            (JoinType::Left, constraint_on(c))
382        }
383        sqlparser::ast::JoinOperator::Right(c) | sqlparser::ast::JoinOperator::RightOuter(c) => {
384            (JoinType::Right, constraint_on(c))
385        }
386        sqlparser::ast::JoinOperator::FullOuter(c) => (JoinType::Full, constraint_on(c)),
387        sqlparser::ast::JoinOperator::CrossJoin(c) => (JoinType::Cross, constraint_on(c)),
388        _ => (JoinType::Inner, String::new()),
389    }
390}
391
392/// Build a scan from a slice of `TableWithJoins` (used by DELETE).
393pub(crate) fn build_scan_from_twjs(twjs: &[sqlparser::ast::TableWithJoins]) -> LogicalPlan {
394    build_from_plan(twjs)
395}
396
397// ── Decorrelation-aware planning ──────────────────────────────────────────────
398
399/// Collect every table name and alias that is reachable from `plan` (used to
400/// determine the outer scope for decorrelation).
401pub(crate) fn collect_outer_scope(plan: &LogicalPlan) -> Vec<String> {
402    let mut out = Vec::new();
403    collect_outer_scope_inner(plan, &mut out);
404    out
405}
406
407fn collect_outer_scope_inner(plan: &LogicalPlan, out: &mut Vec<String>) {
408    match plan {
409        LogicalPlan::Scan { table, alias, .. } => {
410            out.push(table.clone());
411            if let Some(a) = alias {
412                out.push(a.clone());
413            }
414        }
415        LogicalPlan::Filter { input, .. }
416        | LogicalPlan::Project { input, .. }
417        | LogicalPlan::Aggregate { input, .. }
418        | LogicalPlan::Sort { input, .. }
419        | LogicalPlan::Limit { input, .. }
420        | LogicalPlan::Window { input, .. }
421        | LogicalPlan::Compute { input, .. } => collect_outer_scope_inner(input, out),
422        LogicalPlan::Join { left, right, .. } | LogicalPlan::SetOp { left, right, .. } => {
423            collect_outer_scope_inner(left, out);
424            collect_outer_scope_inner(right, out);
425        }
426        _ => {}
427    }
428}
429
430/// Plan a `Statement` with the given [`crate::PlannerOptions`].
431///
432/// For `Query` statements, this uses the decorrelation-aware planning path when
433/// `opts.decorrelate` is `true`.  All other statement kinds fall through to the
434/// standard [`plan_statement`].
435pub fn plan_statement_with_opts(
436    stmt: &sqlparser::ast::Statement,
437    opts: &crate::decorrelate::PlannerOptions,
438) -> Result<LogicalPlan, OxiSqlError> {
439    match stmt {
440        sqlparser::ast::Statement::Query(query) => plan_query_with_opts(query, opts),
441        other => plan_statement(other),
442    }
443}
444
445/// Plan a `Query` AST node with `PlannerOptions`.
446pub(crate) fn plan_query_with_opts(
447    query: &sqlparser::ast::Query,
448    opts: &crate::decorrelate::PlannerOptions,
449) -> Result<LogicalPlan, OxiSqlError> {
450    let mut plan = plan_set_expr_with_opts(&query.body, opts)?;
451
452    // ORDER BY
453    if let Some(ref order_by) = query.order_by {
454        if let sqlparser::ast::OrderByKind::Expressions(ref exprs) = order_by.kind {
455            if !exprs.is_empty() {
456                let sort_keys: Vec<SortExpr> = exprs
457                    .iter()
458                    .map(|e| SortExpr {
459                        column: e.expr.to_string(),
460                        ascending: e.options.asc.unwrap_or(true),
461                    })
462                    .collect();
463                plan = LogicalPlan::Sort {
464                    input: Box::new(plan),
465                    order_by: sort_keys,
466                };
467            }
468        }
469    }
470
471    // LIMIT / OFFSET
472    if let Some(ref limit_clause) = query.limit_clause {
473        let (count, offset) = extract_limit_offset(limit_clause);
474        if count.is_some() || offset.is_some() {
475            plan = LogicalPlan::Limit {
476                input: Box::new(plan),
477                count,
478                offset,
479            };
480        }
481    }
482
483    // WITH / CTE
484    if let Some(ref with) = query.with {
485        let recursive = with.recursive;
486        for cte in with.cte_tables.iter().rev() {
487            let cte_name = cte.alias.name.value.clone();
488            let inner_plan = plan_query_with_opts(&cte.query, opts)?;
489            plan = LogicalPlan::Cte {
490                name: cte_name,
491                query: Box::new(inner_plan),
492                recursive,
493            };
494        }
495    }
496
497    Ok(plan)
498}
499
500fn plan_set_expr_with_opts(
501    body: &sqlparser::ast::SetExpr,
502    opts: &crate::decorrelate::PlannerOptions,
503) -> Result<LogicalPlan, OxiSqlError> {
504    match body {
505        sqlparser::ast::SetExpr::Select(sel) => plan_select_with_opts(sel, opts),
506        sqlparser::ast::SetExpr::Query(inner) => plan_query_with_opts(inner, opts),
507        sqlparser::ast::SetExpr::SetOperation {
508            op,
509            set_quantifier,
510            left,
511            right,
512        } => setops::plan_set_operation(op, set_quantifier, left, right, &|b| {
513            plan_set_expr_with_opts(b, opts)
514        }),
515        _ => Ok(LogicalPlan::Empty),
516    }
517}
518
519/// Plan a `SELECT` with optional decorrelation of correlated subqueries.
520fn plan_select_with_opts(
521    sel: &sqlparser::ast::Select,
522    opts: &crate::decorrelate::PlannerOptions,
523) -> Result<LogicalPlan, OxiSqlError> {
524    // 1 + 2. FROM + JOINs
525    let mut plan = build_from_plan(&sel.from);
526
527    // 3. WHERE
528    if let Some(ref pred) = sel.selection {
529        if opts.decorrelate {
530            let outer_scope = collect_outer_scope(&plan);
531            plan = crate::decorrelate::apply_decorrelated_where(plan, pred, &outer_scope)?;
532        } else {
533            match pred {
534                Expr::Exists { subquery, negated } => {
535                    let inner = plan_query_with_opts(subquery, opts)?;
536                    plan = LogicalPlan::Exists {
537                        subquery: Box::new(inner),
538                        negated: *negated,
539                    };
540                }
541                Expr::InSubquery {
542                    expr,
543                    subquery,
544                    negated,
545                } => {
546                    let inner = plan_query_with_opts(subquery, opts)?;
547                    plan = LogicalPlan::InSubquery {
548                        expr: expr.to_string(),
549                        subquery: Box::new(inner),
550                        negated: *negated,
551                    };
552                }
553                other => {
554                    plan = LogicalPlan::Filter {
555                        input: Box::new(plan),
556                        predicate: other.to_string(),
557                    };
558                }
559            }
560        }
561    }
562
563    // 4. GROUP BY / aggregates
564    let group_by_exprs: Vec<String> = match &sel.group_by {
565        sqlparser::ast::GroupByExpr::Expressions(exprs, _) => {
566            exprs.iter().map(|e| e.to_string()).collect()
567        }
568        sqlparser::ast::GroupByExpr::All(_) => vec!["*".to_string()],
569    };
570    let aggregates: Vec<String> = sel
571        .projection
572        .iter()
573        .filter(|item| agg::projection_item_is_aggregate(item))
574        .map(|item| item.to_string())
575        .collect();
576    if !group_by_exprs.is_empty() || !aggregates.is_empty() {
577        plan = LogicalPlan::Aggregate {
578            input: Box::new(plan),
579            group_by: group_by_exprs,
580            aggregates,
581        };
582        if let Some(ref having_expr) = sel.having {
583            plan = LogicalPlan::Filter {
584                input: Box::new(plan),
585                predicate: having_expr.to_string(),
586            };
587        }
588    }
589
590    // 5. Projection
591    let all_wildcard = sel.projection.iter().all(|item| {
592        matches!(
593            item,
594            sqlparser::ast::SelectItem::Wildcard(_)
595                | sqlparser::ast::SelectItem::QualifiedWildcard(_, _)
596        )
597    });
598    if !all_wildcard
599        && !matches!(plan, LogicalPlan::Aggregate { .. })
600        && !matches!(plan, LogicalPlan::Filter {
601            input: ref i, ..
602        } if matches!(i.as_ref(), LogicalPlan::Aggregate { .. }))
603    {
604        let subquery_plans: Vec<LogicalPlan> = sel
605            .projection
606            .iter()
607            .filter_map(|item| extract_subquery_from_select_item(item).ok().flatten())
608            .collect();
609        if !subquery_plans.is_empty() {
610            for sub_plan in subquery_plans {
611                let alias = sel.projection.iter().find_map(|it| match it {
612                    sqlparser::ast::SelectItem::ExprWithAlias {
613                        expr: Expr::Subquery(_),
614                        alias,
615                    } => Some(alias.value.clone()),
616                    _ => None,
617                });
618                plan = LogicalPlan::Subquery {
619                    query: Box::new(sub_plan),
620                    alias,
621                };
622            }
623        } else {
624            let columns: Vec<String> = sel.projection.iter().map(|item| item.to_string()).collect();
625            plan = LogicalPlan::Project {
626                input: Box::new(plan),
627                columns,
628            };
629        }
630    }
631
632    // 6. Window functions
633    let window_defs: Vec<WindowFunctionDef> = sel
634        .projection
635        .iter()
636        .filter(|item| window::select_item_is_windowed(item))
637        .filter_map(window::extract_window_def)
638        .collect();
639    if !window_defs.is_empty() {
640        plan = LogicalPlan::Window {
641            input: Box::new(plan),
642            functions: window_defs,
643        };
644    }
645
646    Ok(plan)
647}
648
649/// Extract `(count, offset)` from a [`sqlparser::ast::LimitClause`].
650fn extract_limit_offset(clause: &sqlparser::ast::LimitClause) -> (Option<u64>, Option<u64>) {
651    match clause {
652        sqlparser::ast::LimitClause::LimitOffset { limit, offset, .. } => {
653            let count = limit.as_ref().and_then(expr_to_u64);
654            let off = offset.as_ref().and_then(|o| expr_to_u64(&o.value));
655            (count, off)
656        }
657        sqlparser::ast::LimitClause::OffsetCommaLimit { offset, limit } => {
658            let count = expr_to_u64(limit);
659            let off = expr_to_u64(offset);
660            (count, Some(off.unwrap_or(0)))
661        }
662    }
663}
664
665/// Try to extract a `u64` from a literal `Expr::Value(Number(..))`.
666fn expr_to_u64(expr: &Expr) -> Option<u64> {
667    if let Expr::Value(sqlparser::ast::ValueWithSpan {
668        value: sqlparser::ast::Value::Number(ref s, _),
669        ..
670    }) = expr
671    {
672        s.parse::<u64>().ok()
673    } else {
674        None
675    }
676}