Skip to main content

nodedb_sql/planner/lateral/
subquery.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Extraction over a LATERAL subquery's AST: its inner relation, its
4//! non-correlated filters, and its row bounds.
5
6use sqlparser::ast;
7
8use crate::error::{Result, SqlError};
9use crate::parser::normalize::normalize_ident;
10use crate::reserved::check_ast_identifier;
11use crate::resolver::columns::TableScope;
12use crate::types::Filter;
13
14/// Extract the alias of the single-table inner SELECT, if present.
15pub(super) fn extract_inner_alias(select: &sqlparser::ast::Select) -> Result<Option<String>> {
16    let Some(from) = select.from.first() else {
17        return Ok(None);
18    };
19    match &from.relation {
20        ast::TableFactor::Table { alias, .. } => alias
21            .as_ref()
22            .map(|alias| check_ast_identifier(&alias.name))
23            .transpose(),
24        _ => Ok(None),
25    }
26}
27
28/// Extract the collection name from a single-table inner SELECT.
29pub(super) fn extract_inner_collection(select: &sqlparser::ast::Select) -> Result<String> {
30    let from = select.from.first().ok_or_else(|| SqlError::Unsupported {
31        detail: "LATERAL subquery must have a FROM clause".into(),
32    })?;
33    crate::parser::normalize::table_name_from_factor(&from.relation)?
34        .map(|(name, _)| name)
35        .ok_or_else(|| SqlError::Unsupported {
36            detail: "LATERAL LateralTopK subquery must reference a plain table".into(),
37        })
38}
39
40/// Extract filters from the inner SELECT that do NOT reference the outer alias.
41pub(super) fn inner_non_correlated_filters(
42    select: &sqlparser::ast::Select,
43    outer_alias: &str,
44    scope: &TableScope,
45) -> Result<Vec<Filter>> {
46    let Some(where_expr) = &select.selection else {
47        return Ok(Vec::new());
48    };
49    let remaining = strip_outer_refs(where_expr, outer_alias);
50    match remaining {
51        Some(expr) => crate::planner::select::convert_where_to_filters(&expr, scope),
52        None => Ok(Vec::new()),
53    }
54}
55
56/// Remove all predicates referencing `outer_alias` from a WHERE expression.
57fn strip_outer_refs(expr: &ast::Expr, outer_alias: &str) -> Option<ast::Expr> {
58    match expr {
59        ast::Expr::BinaryOp {
60            left,
61            op: ast::BinaryOperator::And,
62            right,
63        } => {
64            let l = strip_outer_refs(left, outer_alias);
65            let r = strip_outer_refs(right, outer_alias);
66            match (l, r) {
67                (None, None) => None,
68                (Some(e), None) | (None, Some(e)) => Some(e),
69                (Some(l), Some(r)) => Some(ast::Expr::BinaryOp {
70                    left: Box::new(l),
71                    op: ast::BinaryOperator::And,
72                    right: Box::new(r),
73                }),
74            }
75        }
76        ast::Expr::BinaryOp { left, right, .. } => {
77            if refs_outer(left, outer_alias) || refs_outer(right, outer_alias) {
78                None
79            } else {
80                Some(expr.clone())
81            }
82        }
83        ast::Expr::Nested(inner) => strip_outer_refs(inner, outer_alias),
84        _ => Some(expr.clone()),
85    }
86}
87
88fn refs_outer(expr: &ast::Expr, outer_alias: &str) -> bool {
89    match expr {
90        ast::Expr::CompoundIdentifier(parts) if parts.len() == 2 => {
91            normalize_ident(&parts[0]).eq_ignore_ascii_case(outer_alias)
92        }
93        ast::Expr::BinaryOp { left, right, .. } => {
94            refs_outer(left, outer_alias) || refs_outer(right, outer_alias)
95        }
96        _ => false,
97    }
98}
99
100/// Extract the LIMIT value from a query, or fail on a bound that does not
101/// resolve to `[0, usize::MAX]`. `LIMIT NULL` / `LIMIT ALL` and an absent
102/// clause all mean no bound, so both map to `None`.
103pub(super) fn limit_from_query(query: &ast::Query) -> Result<Option<usize>> {
104    match &query.limit_clause {
105        Some(ast::LimitClause::LimitOffset {
106            limit: Some(limit), ..
107        })
108        | Some(ast::LimitClause::OffsetCommaLimit { limit, .. }) => {
109            Ok(crate::coerce::checked_row_bound("LIMIT", limit)?.limit())
110        }
111        Some(ast::LimitClause::LimitOffset { limit: None, .. }) | None => Ok(None),
112    }
113}
114
115/// Reject an inner OFFSET on a LATERAL subquery.
116///
117/// `SqlPlan::LateralTopK` carries no offset field and `SqlPlan::LateralLoop`
118/// carries neither limit nor offset. A per-outer-row OFFSET needs a new plan
119/// field plus Data Plane execution that skips rows per outer row, so this
120/// rejects rather than silently drops the clause. `OFFSET 0` and `OFFSET
121/// NULL` skip nothing and plan cleanly; a resolved offset above zero fails
122/// with `SqlError::Unsupported`. An offset literal outside `[0, usize::MAX]`
123/// fails first, inside `checked_row_bound`, with `SqlError::InvalidLimitValue`.
124pub(super) fn reject_lateral_offset(query: &ast::Query) -> Result<()> {
125    let offset_expr = match &query.limit_clause {
126        Some(ast::LimitClause::LimitOffset {
127            offset: Some(offset),
128            ..
129        }) => Some(&offset.value),
130        Some(ast::LimitClause::OffsetCommaLimit { offset, .. }) => Some(offset),
131        Some(ast::LimitClause::LimitOffset { offset: None, .. }) | None => None,
132    };
133    let Some(expr) = offset_expr else {
134        return Ok(());
135    };
136    if crate::coerce::checked_row_bound("OFFSET", expr)?.offset() > 0 {
137        return Err(SqlError::Unsupported {
138            detail: "OFFSET inside a LATERAL subquery is not supported".into(),
139        });
140    }
141    Ok(())
142}
143
144/// Extract and validate a LATERAL alias from a `TableFactor::Derived`.
145pub fn lateral_alias_from_factor(factor: &ast::TableFactor) -> Result<Option<String>> {
146    match factor {
147        ast::TableFactor::Derived { alias, .. } => alias
148            .as_ref()
149            .map(|alias| check_ast_identifier(&alias.name))
150            .transpose(),
151        _ => Ok(None),
152    }
153}
154
155/// True when a `TableFactor` is a LATERAL derived subquery.
156pub fn is_lateral_derived(factor: &ast::TableFactor) -> bool {
157    matches!(factor, ast::TableFactor::Derived { lateral: true, .. })
158}
159
160/// Extract the subquery from a `TableFactor::Derived`.
161pub fn subquery_from_factor(factor: &ast::TableFactor) -> Option<&ast::Query> {
162    match factor {
163        ast::TableFactor::Derived { subquery, .. } => Some(subquery),
164        _ => None,
165    }
166}