Skip to main content

nodedb_sql/planner/subquery/
extract.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! The WHERE-clause walk that pulls subquery predicates out into joins.
4//!
5//! Rewrites WHERE-clause subqueries into semi/anti joins so the existing
6//! hash-join executor handles them without a dedicated subquery engine.
7//!
8//! Supported patterns:
9//!   - `WHERE col IN (SELECT col2 FROM tbl ...)` → semi-join
10//!   - `WHERE col NOT IN (SELECT col2 FROM tbl ...)` → anti-join
11//!   - `WHERE EXISTS (SELECT ... )` → semi-join
12//!   - `WHERE NOT EXISTS (SELECT ... )` → anti-join
13//!   - `WHERE col > (SELECT AGG(...) FROM tbl ...)` → scalar subquery (materialized)
14
15use sqlparser::ast::{self, Expr};
16
17use crate::error::Result;
18use crate::functions::registry::FunctionRegistry;
19use crate::resolver::columns::TableScope;
20use crate::types::*;
21
22/// Result of extracting subqueries from a WHERE clause.
23pub struct SubqueryExtraction {
24    /// Semi/anti joins to wrap around the base scan.
25    pub joins: Vec<SubqueryJoin>,
26    /// Remaining WHERE expression with subqueries removed (None if nothing remains).
27    pub remaining_where: Option<Expr>,
28}
29
30/// A subquery that was rewritten as a join.
31pub struct SubqueryJoin {
32    /// Equi-join keys as `(outer column, inner column)` pairs. Empty for an
33    /// uncorrelated subquery: the probe then treats every inner row as a
34    /// candidate, which is what `EXISTS` over an unrelated table means.
35    pub on: Vec<(String, String)>,
36    /// The planned inner SELECT.
37    pub inner_plan: SqlPlan,
38    /// Semi (IN / EXISTS) or Anti (NOT IN / NOT EXISTS).
39    pub join_type: JoinType,
40}
41
42/// Extract subquery predicates from a WHERE clause.
43///
44/// `outer` is the scope of the enclosing query, so a correlated reference
45/// inside a subquery resolves against the relation that owns it.
46///
47/// Returns the extracted subquery joins and the remaining WHERE expression
48/// (with subquery predicates removed). If the entire WHERE is a single
49/// subquery predicate, `remaining_where` is `None`.
50pub fn extract_subqueries(
51    expr: &Expr,
52    outer: &TableScope,
53    catalog: &dyn SqlCatalog,
54    functions: &FunctionRegistry,
55    temporal: crate::TemporalScope,
56) -> Result<SubqueryExtraction> {
57    let mut joins = Vec::new();
58    let remaining = extract_recursive(expr, &mut joins, outer, catalog, functions, temporal)?;
59    Ok(SubqueryExtraction {
60        joins,
61        remaining_where: remaining,
62    })
63}
64
65/// Recursively walk the WHERE expression, extracting subquery predicates.
66///
67/// Returns `None` if the entire expression was consumed (subquery-only),
68/// or `Some(expr)` with the remaining non-subquery predicates.
69fn extract_recursive(
70    expr: &Expr,
71    joins: &mut Vec<SubqueryJoin>,
72    outer: &TableScope,
73    catalog: &dyn SqlCatalog,
74    functions: &FunctionRegistry,
75    temporal: crate::TemporalScope,
76) -> Result<Option<Expr>> {
77    match expr {
78        // AND: recurse both sides, reconstruct with remaining parts.
79        Expr::BinaryOp {
80            left,
81            op: ast::BinaryOperator::And,
82            right,
83        } => {
84            let left_remaining =
85                extract_recursive(left, joins, outer, catalog, functions, temporal)?;
86            let right_remaining =
87                extract_recursive(right, joins, outer, catalog, functions, temporal)?;
88            match (left_remaining, right_remaining) {
89                (None, None) => Ok(None),
90                (Some(l), None) => Ok(Some(l)),
91                (None, Some(r)) => Ok(Some(r)),
92                (Some(l), Some(r)) => Ok(Some(Expr::BinaryOp {
93                    left: Box::new(l),
94                    op: ast::BinaryOperator::And,
95                    right: Box::new(r),
96                })),
97            }
98        }
99
100        // IN (SELECT ...): rewrite as semi-join.
101        Expr::InSubquery {
102            expr: outer_expr,
103            subquery,
104            negated,
105        } => {
106            if let Some(join) = super::in_list::try_plan_in_subquery(
107                outer_expr, subquery, *negated, outer, catalog, functions, temporal,
108            )? {
109                joins.push(join);
110                Ok(None) // This predicate is consumed.
111            } else {
112                // Cannot plan as join — return original expression.
113                Ok(Some(expr.clone()))
114            }
115        }
116
117        // Scalar subquery comparison: `col > (SELECT AGG(...) FROM ...)`
118        Expr::BinaryOp { left, op, right } if is_comparison_op(op) => {
119            if let Expr::Subquery(subquery) = right.as_ref() {
120                if let Some(scalar) =
121                    super::scalar::try_plan_scalar_subquery(subquery, catalog, functions, temporal)?
122                {
123                    joins.push(scalar.join);
124                    Ok(Some(Expr::BinaryOp {
125                        left: left.clone(),
126                        op: op.clone(),
127                        right: Box::new(scalar.replacement_expr),
128                    }))
129                } else {
130                    Ok(Some(expr.clone()))
131                }
132            } else {
133                Ok(Some(expr.clone()))
134            }
135        }
136
137        // EXISTS (SELECT ...): rewrite as semi-join.
138        // NOT EXISTS (SELECT ...): rewrite as anti-join.
139        //
140        // A shape the planner cannot lower raises a typed error naming that
141        // shape. Leaving the node in the residual WHERE would instead surface
142        // it as an unsupported *expression*, which says nothing actionable.
143        Expr::Exists { subquery, negated } => {
144            joins.push(super::exists::plan_exists_subquery(
145                subquery, *negated, outer, catalog, functions, temporal,
146            )?);
147            Ok(None)
148        }
149
150        // Nested parentheses.
151        Expr::Nested(inner) => extract_recursive(inner, joins, outer, catalog, functions, temporal),
152
153        // Not a subquery pattern — return as-is.
154        _ => Ok(Some(expr.clone())),
155    }
156}
157
158fn is_comparison_op(op: &ast::BinaryOperator) -> bool {
159    matches!(
160        op,
161        ast::BinaryOperator::Gt
162            | ast::BinaryOperator::GtEq
163            | ast::BinaryOperator::Lt
164            | ast::BinaryOperator::LtEq
165            | ast::BinaryOperator::Eq
166            | ast::BinaryOperator::NotEq
167    )
168}