Skip to main content

nodedb_sql/planner/join/
plan.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! JOIN planning entry: builds left + right scans (or array-TVF arms),
4//! attaches projection/filters/aggregation.
5
6use sqlparser::ast::{self, Select};
7
8use super::array_arm;
9use super::constraint::extract_join_spec;
10use crate::error::{Result, SqlError};
11use crate::functions::registry::FunctionRegistry;
12use crate::planner::lateral::plan::{
13    is_lateral_derived, lateral_alias_from_factor, plan_lateral_join, subquery_from_factor,
14};
15use crate::resolver::columns::TableScope;
16use crate::types::*;
17
18pub fn plan_join_from_select(
19    select: &Select,
20    scope: &TableScope,
21    catalog: &dyn SqlCatalog,
22    functions: &FunctionRegistry,
23    temporal: crate::TemporalScope,
24) -> Result<Option<SqlPlan>> {
25    let from = &select.from[0];
26
27    // Left side: either an ARRAY_* TVF or a named table.
28    let left_plan =
29        if let Some(plan) = array_arm::try_plan_relation(&from.relation, catalog, temporal)? {
30            plan
31        } else {
32            scan_for_relation(&from.relation, scope)?
33        };
34
35    let outer_alias = scan_alias_from_relation(&from.relation);
36
37    let mut current_plan = left_plan;
38
39    for join_item in &from.joins {
40        // Detect LATERAL derived subquery on the right side.
41        if is_lateral_derived(&join_item.relation) {
42            let lateral_alias =
43                lateral_alias_from_factor(&join_item.relation).ok_or_else(|| {
44                    SqlError::Unsupported {
45                        detail: "LATERAL subquery requires an alias (e.g. LATERAL (...) AS x)"
46                            .into(),
47                    }
48                })?;
49            let subquery = subquery_from_factor(&join_item.relation)
50                .expect("is_lateral_derived guarantees Derived variant");
51            let left_join = is_left_join_operator(&join_item.join_operator);
52            let projection = super::super::select::convert_projection(&select.projection)?;
53            return Ok(Some(plan_lateral_join(
54                current_plan,
55                outer_alias,
56                subquery,
57                &lateral_alias,
58                left_join,
59                projection,
60                catalog,
61                functions,
62                temporal,
63            )?));
64        }
65
66        // Right side: array TVF or named table.
67        let right_plan = if let Some(plan) =
68            array_arm::try_plan_relation(&join_item.relation, catalog, temporal)?
69        {
70            plan
71        } else {
72            scan_for_relation(&join_item.relation, scope)?
73        };
74
75        let (join_type, on_keys, condition) = extract_join_spec(&join_item.join_operator)?;
76
77        current_plan = SqlPlan::Join {
78            left: Box::new(current_plan),
79            right: Box::new(right_plan),
80            on: on_keys,
81            join_type,
82            condition,
83            limit: 10000,
84            projection: Vec::new(),
85            filters: Vec::new(),
86        };
87    }
88
89    let (subquery_joins, effective_where) = if let Some(expr) = &select.selection {
90        let extraction =
91            super::super::subquery::extract_subqueries(expr, catalog, functions, temporal)?;
92        (extraction.joins, extraction.remaining_where)
93    } else {
94        (Vec::new(), None)
95    };
96
97    let projection = super::super::select::convert_projection(&select.projection)?;
98    let filters = match &effective_where {
99        Some(expr) => super::super::select::convert_where_to_filters(expr)?,
100        None => Vec::new(),
101    };
102
103    for sq in subquery_joins {
104        current_plan = SqlPlan::Join {
105            left: Box::new(current_plan),
106            right: Box::new(sq.inner_plan),
107            on: vec![(sq.outer_column, sq.inner_column)],
108            join_type: sq.join_type,
109            condition: None,
110            limit: 10000,
111            projection: Vec::new(),
112            filters: Vec::new(),
113        };
114    }
115
116    let group_by_non_empty = match &select.group_by {
117        ast::GroupByExpr::All(_) => true,
118        ast::GroupByExpr::Expressions(exprs, _) => !exprs.is_empty(),
119    };
120    if super::super::select::convert_projection(&select.projection).is_ok() && group_by_non_empty {
121        let aggregates = super::super::aggregate::extract_aggregates_from_projection(
122            &select.projection,
123            functions,
124        )?;
125        let group_by = super::super::aggregate::convert_group_by(&select.group_by)?;
126        let having = match &select.having {
127            Some(expr) => super::super::select::convert_where_to_filters(expr)?,
128            None => Vec::new(),
129        };
130        return Ok(Some(SqlPlan::Aggregate {
131            input: Box::new(current_plan),
132            group_by,
133            aggregates,
134            having,
135            limit: 10000,
136            grouping_sets: None,
137            sort_keys: Vec::new(),
138        }));
139    }
140
141    if let SqlPlan::Join {
142        projection: ref mut proj,
143        filters: ref mut filt,
144        ..
145    } = current_plan
146    {
147        *proj = projection;
148        *filt = filters;
149    }
150    Ok(Some(current_plan))
151}
152
153/// Extract an alias (or table name) from a named-table `TableFactor`.
154fn scan_alias_from_relation(factor: &ast::TableFactor) -> Option<String> {
155    match factor {
156        ast::TableFactor::Table { name, alias, .. } => alias
157            .as_ref()
158            .map(|a| crate::parser::normalize::normalize_ident(&a.name))
159            .or_else(|| crate::parser::normalize::normalize_object_name_checked(name).ok()),
160        _ => None,
161    }
162}
163
164/// True when the join operator represents a LEFT join variant.
165fn is_left_join_operator(op: &ast::JoinOperator) -> bool {
166    matches!(
167        op,
168        ast::JoinOperator::Left(_) | ast::JoinOperator::LeftOuter(_)
169    )
170}
171
172/// Build a `SqlPlan::Scan` for a named-table TableFactor.
173fn scan_for_relation(rel: &ast::TableFactor, scope: &TableScope) -> Result<SqlPlan> {
174    let (rel_name, rel_alias) =
175        crate::parser::normalize::table_name_from_factor(rel)?.ok_or_else(|| {
176            SqlError::Unsupported {
177                detail: "non-table JOIN target".into(),
178            }
179        })?;
180    let table = scope
181        .tables
182        .values()
183        .find(|t| t.name == rel_name || t.alias.as_deref() == Some(&rel_name))
184        .ok_or_else(|| SqlError::UnknownTable {
185            name: rel_name.clone(),
186        })?;
187    Ok(SqlPlan::Scan {
188        collection: table.name.clone(),
189        alias: rel_alias.or_else(|| table.alias.clone()),
190        engine: table.info.engine,
191        filters: Vec::new(),
192        projection: Vec::new(),
193        sort_keys: Vec::new(),
194        limit: None,
195        offset: 0,
196        distinct: false,
197        window_functions: Vec::new(),
198        temporal: crate::temporal::TemporalScope::default(),
199    })
200}