Skip to main content

nodedb_sql/planner/subquery/
in_list.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! `IN (SELECT ...)` and `NOT IN (SELECT ...)` lowered to semi / anti joins.
4
5use sqlparser::ast::{self, Expr, SetExpr};
6
7use crate::error::{Result, SqlError};
8use crate::functions::registry::FunctionRegistry;
9use crate::parser::normalize::{SCHEMA_QUALIFIED_MSG, normalize_ident};
10use crate::types::*;
11
12use super::extract::SubqueryJoin;
13
14/// Try to plan `col IN (SELECT col2 FROM tbl ...)` as a semi/anti join.
15pub(super) fn try_plan_in_subquery(
16    outer_expr: &Expr,
17    subquery: &ast::Query,
18    negated: bool,
19    outer: &crate::resolver::columns::TableScope,
20    catalog: &dyn SqlCatalog,
21    functions: &FunctionRegistry,
22    temporal: crate::TemporalScope,
23) -> Result<Option<SubqueryJoin>> {
24    // This rewrite consumes the whole predicate, so the outer operand never
25    // reaches the expression converter. It is checked here or nowhere.
26    let outer_col = match outer_expr {
27        Expr::Identifier(ident) => {
28            let col = normalize_ident(ident);
29            outer.check_name(None, &col)?;
30            col
31        }
32        Expr::CompoundIdentifier(parts) if parts.len() >= 3 => {
33            let qualified: String = parts
34                .iter()
35                .map(normalize_ident)
36                .collect::<Vec<_>>()
37                .join(".");
38            return Err(SqlError::Unsupported {
39                detail: format!(
40                    "schema-qualified column reference '{qualified}': {SCHEMA_QUALIFIED_MSG}"
41                ),
42            });
43        }
44        Expr::CompoundIdentifier(parts) if parts.len() == 2 => {
45            let qualifier = normalize_ident(&parts[0]);
46            let col = normalize_ident(&parts[1]);
47            outer.check_name(Some(&qualifier), &col)?;
48            col
49        }
50        _ => return Ok(None), // Complex expression, can't rewrite.
51    };
52
53    // Plan the inner SELECT.
54    let inner_plan = crate::planner::select::plan_query(subquery, catalog, functions, temporal)?;
55
56    // Extract the projected column from the inner plan.
57    let inner_col = extract_single_projected_column(subquery)?;
58
59    Ok(Some(SubqueryJoin {
60        on: vec![(outer_col, inner_col)],
61        inner_plan,
62        join_type: if negated {
63            JoinType::Anti
64        } else {
65            JoinType::Semi
66        },
67    }))
68}
69
70/// Extract the single column name from a subquery's SELECT list.
71///
72/// For `SELECT user_id FROM orders`, returns `"user_id"`.
73fn extract_single_projected_column(query: &ast::Query) -> Result<String> {
74    let select = match &*query.body {
75        SetExpr::Select(s) => s,
76        _ => {
77            return Err(SqlError::Unsupported {
78                detail: "subquery must be a simple SELECT".into(),
79            });
80        }
81    };
82
83    if select.projection.len() != 1 {
84        return Err(SqlError::Unsupported {
85            detail: format!(
86                "subquery must select exactly 1 column, got {}",
87                select.projection.len()
88            ),
89        });
90    }
91
92    match &select.projection[0] {
93        ast::SelectItem::UnnamedExpr(expr) => match expr {
94            Expr::Identifier(ident) => Ok(normalize_ident(ident)),
95            Expr::CompoundIdentifier(parts) if parts.len() >= 3 => {
96                let qualified: String = parts
97                    .iter()
98                    .map(normalize_ident)
99                    .collect::<Vec<_>>()
100                    .join(".");
101                Err(SqlError::Unsupported {
102                    detail: format!(
103                        "schema-qualified column reference '{qualified}': {SCHEMA_QUALIFIED_MSG}"
104                    ),
105                })
106            }
107            Expr::CompoundIdentifier(parts) if parts.len() == 2 => Ok(normalize_ident(&parts[1])),
108            _ => Err(SqlError::Unsupported {
109                detail: "subquery projection must be a column reference".into(),
110            }),
111        },
112        ast::SelectItem::ExprWithAlias { alias, .. } => Ok(normalize_ident(alias)),
113        _ => Err(SqlError::Unsupported {
114            detail: "subquery projection must be a column reference".into(),
115        }),
116    }
117}