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    LateralJoinArgs, is_lateral_derived, lateral_alias_from_factor, plan_lateral_join,
14    subquery_from_factor,
15};
16use crate::resolver::columns::TableScope;
17use crate::types::*;
18
19pub fn plan_join_from_select(
20    select: &Select,
21    scope: &TableScope,
22    catalog: &dyn SqlCatalog,
23    functions: &FunctionRegistry,
24    temporal: crate::TemporalScope,
25) -> Result<Option<SqlPlan>> {
26    let from = &select.from[0];
27
28    // Left side: either an ARRAY_* TVF or a named table.
29    let left_plan =
30        if let Some(plan) = array_arm::try_plan_relation(&from.relation, catalog, temporal)? {
31            plan
32        } else {
33            scan_for_relation(&from.relation, scope)?
34        };
35
36    let outer_alias = scan_alias_from_relation(&from.relation);
37
38    let mut current_plan = left_plan;
39
40    for join_item in &from.joins {
41        // Detect LATERAL derived subquery on the right side.
42        if is_lateral_derived(&join_item.relation) {
43            let lateral_alias =
44                lateral_alias_from_factor(&join_item.relation).ok_or_else(|| {
45                    SqlError::Unsupported {
46                        detail: "LATERAL subquery requires an alias (e.g. LATERAL (...) AS x)"
47                            .into(),
48                    }
49                })?;
50            let subquery = subquery_from_factor(&join_item.relation)
51                .expect("is_lateral_derived guarantees Derived variant");
52            let left_join = is_left_join_operator(&join_item.join_operator);
53            let projection = super::super::select::convert_projection(&select.projection)?;
54            return Ok(Some(plan_lateral_join(LateralJoinArgs {
55                outer_plan: current_plan,
56                outer_alias,
57                subquery,
58                lateral_alias: &lateral_alias,
59                left_join,
60                outer_projection: projection,
61                catalog,
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, mut on_keys, condition) = extract_join_spec(&join_item.join_operator)?;
76
77        // Orient equi-keys to FROM order: `on.0` must reference the left input
78        // and `on.1` the right input. The ON clause may write the operands in
79        // either order (`ON right.k = left.k`); the physical join builds the
80        // hash index on the right key, so a reversed pair would index the wrong
81        // side and match zero rows.
82        let right_ids = right_side_identifiers(&join_item.relation);
83        super::constraint::orient_keys_to_sides(&mut on_keys, &right_ids);
84
85        current_plan = SqlPlan::Join {
86            left: Box::new(current_plan),
87            right: Box::new(right_plan),
88            on: on_keys,
89            join_type,
90            condition,
91            limit: None,
92            projection: Vec::new(),
93            filters: Vec::new(),
94        };
95    }
96
97    let (subquery_joins, effective_where) = if let Some(expr) = &select.selection {
98        let extraction =
99            super::super::subquery::extract_subqueries(expr, catalog, functions, temporal)?;
100        (extraction.joins, extraction.remaining_where)
101    } else {
102        (Vec::new(), None)
103    };
104
105    let projection = super::super::select::convert_projection(&select.projection)?;
106    let filters = match &effective_where {
107        Some(expr) => super::super::select::convert_where_to_filters(expr)?,
108        None => Vec::new(),
109    };
110
111    for sq in subquery_joins {
112        current_plan = SqlPlan::Join {
113            left: Box::new(current_plan),
114            right: Box::new(sq.inner_plan),
115            on: vec![(sq.outer_column, sq.inner_column)],
116            join_type: sq.join_type,
117            condition: None,
118            limit: None,
119            projection: Vec::new(),
120            filters: Vec::new(),
121        };
122    }
123
124    let group_by_non_empty = match &select.group_by {
125        ast::GroupByExpr::All(_) => true,
126        ast::GroupByExpr::Expressions(exprs, _) => !exprs.is_empty(),
127    };
128    if super::super::select::convert_projection(&select.projection).is_ok() && group_by_non_empty {
129        let aggregates = super::super::aggregate::extract_aggregates_from_projection(
130            &select.projection,
131            functions,
132        )?;
133        let group_by = super::super::aggregate::convert_group_by(&select.group_by)?;
134        let group_by_aliases =
135            super::super::aggregate::group_by_output_aliases(&select.projection, &group_by);
136        let output_order = super::super::aggregate_order::compute_output_order(
137            &select.projection,
138            &group_by,
139            functions,
140        )?;
141        let having = match &select.having {
142            Some(expr) => super::super::select::convert_where_to_filters(expr)?,
143            None => Vec::new(),
144        };
145        return Ok(Some(SqlPlan::Aggregate {
146            input: Box::new(current_plan),
147            group_by,
148            group_by_aliases,
149            output_order,
150            aggregates,
151            having,
152            limit: 10000,
153            grouping_sets: None,
154            sort_keys: Vec::new(),
155        }));
156    }
157
158    if let SqlPlan::Join {
159        projection: ref mut proj,
160        filters: ref mut filt,
161        ..
162    } = current_plan
163    {
164        *proj = projection;
165        *filt = filters;
166    }
167    Ok(Some(current_plan))
168}
169
170/// Collect the normalized identifiers (alias and/or table name) by which a
171/// right-side join relation can be referenced in an ON clause. Used to orient
172/// equi-keys so the right-side operand is always `on.1`.
173fn right_side_identifiers(factor: &ast::TableFactor) -> Vec<String> {
174    let mut ids = Vec::new();
175    if let Ok(Some((name, alias))) = crate::parser::normalize::table_name_from_factor(factor) {
176        if let Some(a) = alias {
177            ids.push(a);
178        }
179        ids.push(name);
180    }
181    ids
182}
183
184/// Extract an alias (or table name) from a named-table `TableFactor`.
185fn scan_alias_from_relation(factor: &ast::TableFactor) -> Option<String> {
186    match factor {
187        ast::TableFactor::Table { name, alias, .. } => alias
188            .as_ref()
189            .map(|a| crate::parser::normalize::normalize_ident(&a.name))
190            .or_else(|| crate::parser::normalize::normalize_object_name_checked(name).ok()),
191        _ => None,
192    }
193}
194
195/// True when the join operator represents a LEFT join variant.
196fn is_left_join_operator(op: &ast::JoinOperator) -> bool {
197    matches!(
198        op,
199        ast::JoinOperator::Left(_) | ast::JoinOperator::LeftOuter(_)
200    )
201}
202
203/// Build a `SqlPlan::Scan` for a named-table TableFactor.
204fn scan_for_relation(rel: &ast::TableFactor, scope: &TableScope) -> Result<SqlPlan> {
205    let (rel_name, rel_alias) =
206        crate::parser::normalize::table_name_from_factor(rel)?.ok_or_else(|| {
207            SqlError::Unsupported {
208                detail: "non-table JOIN target".into(),
209            }
210        })?;
211    let table = scope
212        .tables
213        .values()
214        .find(|t| t.name == rel_name || t.alias.as_deref() == Some(&rel_name))
215        .ok_or_else(|| SqlError::UnknownTable {
216            name: rel_name.clone(),
217        })?;
218    Ok(SqlPlan::Scan {
219        collection: table.name.clone(),
220        alias: rel_alias.or_else(|| table.alias.clone()),
221        engine: table.info.engine,
222        filters: Vec::new(),
223        projection: Vec::new(),
224        sort_keys: Vec::new(),
225        limit: None,
226        offset: 0,
227        distinct: false,
228        window_functions: Vec::new(),
229        temporal: crate::temporal::TemporalScope::default(),
230    })
231}