nodedb_sql/planner/subquery/
exists.rs1use std::ops::ControlFlow;
10
11use sqlparser::ast::{self, SetExpr};
12
13use crate::error::{Result, SqlError};
14use crate::functions::registry::FunctionRegistry;
15use crate::parser::normalize::normalize_ident;
16use crate::planner::ast_helpers::{flatten_and_expr, rebuild_and_expr};
17use crate::planner::select::{convert_projection, plan_query};
18use crate::resolver::ColumnScope;
19use crate::resolver::columns::TableScope;
20use crate::resolver::expr::convert_expr;
21use crate::types::*;
22
23use super::extract::SubqueryJoin;
24
25const SET_OPERATION: &str = "EXISTS over a set operation (UNION / INTERSECT / EXCEPT) \
26 is not supported; wrap the set operation in a derived table and select from that";
27
28const OUTER_ONLY_EQUALITY: &str = "an equality naming only outer relations inside EXISTS \
29 is not supported; move it to the enclosing WHERE clause";
30
31const CORRELATION_UNDER_OR: &str = "a correlated reference under OR inside EXISTS \
32 is not supported; each correlation must be an AND-ed equality";
33
34pub(super) fn plan_exists_subquery(
40 subquery: &ast::Query,
41 negated: bool,
42 outer: &TableScope,
43 catalog: &dyn SqlCatalog,
44 functions: &FunctionRegistry,
45 temporal: crate::TemporalScope,
46) -> Result<SubqueryJoin> {
47 let SetExpr::Select(select) = &*subquery.body else {
48 return Err(SqlError::Unsupported {
49 detail: SET_OPERATION.into(),
50 });
51 };
52
53 let local = TableScope::resolve_from(catalog, functions, temporal, &select.from)?;
54 let nested = local.clone().nested_in(outer.clone());
55
56 convert_projection(&select.projection, &nested)?;
59
60 let mut on = Vec::new();
61 let mut local_conjuncts = Vec::new();
62 if let Some(where_expr) = &select.selection {
63 let mut conjuncts = Vec::new();
64 flatten_and_expr(where_expr, &mut conjuncts);
65 for conjunct in conjuncts {
66 convert_expr(&conjunct, &ColumnScope::Relations(&nested))?;
69 match correlation_pair(&conjunct, &local, outer)? {
70 Some(pair) => on.push(pair),
71 None => local_conjuncts.push(conjunct),
72 }
73 }
74 }
75
76 let inner_plan = plan_local_query(
77 subquery,
78 select,
79 local_conjuncts,
80 catalog,
81 functions,
82 temporal,
83 )?;
84
85 Ok(SubqueryJoin {
86 on,
87 inner_plan,
88 join_type: if negated {
89 JoinType::Anti
90 } else {
91 JoinType::Semi
92 },
93 })
94}
95
96fn plan_local_query(
101 subquery: &ast::Query,
102 select: &ast::Select,
103 local_conjuncts: Vec<ast::Expr>,
104 catalog: &dyn SqlCatalog,
105 functions: &FunctionRegistry,
106 temporal: crate::TemporalScope,
107) -> Result<SqlPlan> {
108 let mut inner_select = select.clone();
109 inner_select.projection = vec![ast::SelectItem::Wildcard(
110 ast::WildcardAdditionalOptions::default(),
111 )];
112 inner_select.selection = if local_conjuncts.is_empty() {
113 None
114 } else {
115 Some(rebuild_and_expr(local_conjuncts))
116 };
117
118 let mut inner_query = subquery.clone();
119 *inner_query.body = SetExpr::Select(Box::new(inner_select));
120 plan_query(&inner_query, catalog, functions, temporal)
121}
122
123enum Side {
125 Inner(String),
127 Outer(String),
129 Other,
131}
132
133fn correlation_pair(
139 expr: &ast::Expr,
140 inner: &TableScope,
141 outer: &TableScope,
142) -> Result<Option<(String, String)>> {
143 let bare = unnest(expr);
144 if let ast::Expr::BinaryOp { left, op, right } = bare {
145 let l = classify(left, inner, outer);
146 let r = classify(right, inner, outer);
147 match (op, &l, &r) {
148 (ast::BinaryOperator::Eq, Side::Inner(ic), Side::Outer(oc))
149 | (ast::BinaryOperator::Eq, Side::Outer(oc), Side::Inner(ic)) => {
150 return Ok(Some((oc.clone(), ic.clone())));
151 }
152 (ast::BinaryOperator::Eq, Side::Outer(_), Side::Outer(_)) => {
153 return Err(SqlError::Unsupported {
154 detail: OUTER_ONLY_EQUALITY.into(),
155 });
156 }
157 (ast::BinaryOperator::Or, _, _)
158 if references_outer(left, inner, outer)
159 || references_outer(right, inner, outer) =>
160 {
161 return Err(SqlError::Unsupported {
162 detail: CORRELATION_UNDER_OR.into(),
163 });
164 }
165 (op, Side::Inner(_), Side::Outer(_)) | (op, Side::Outer(_), Side::Inner(_))
166 if is_inequality(op) =>
167 {
168 return Err(SqlError::Unsupported {
169 detail: format!(
170 "a correlated inequality ('{op}') inside EXISTS is not supported; \
171 a semi join probes on equality keys only"
172 ),
173 });
174 }
175 _ => {}
176 }
177 }
178
179 if references_outer(bare, inner, outer) {
180 return Err(SqlError::Unsupported {
181 detail: format!(
182 "the correlated predicate '{expr}' inside EXISTS is not supported; \
183 a correlation must be an AND-ed equality between one outer column \
184 and one inner column"
185 ),
186 });
187 }
188 Ok(None)
189}
190
191fn classify(expr: &ast::Expr, inner: &TableScope, outer: &TableScope) -> Side {
197 match expr {
198 ast::Expr::CompoundIdentifier(parts) if parts.len() == 2 => {
199 let qualifier = normalize_ident(&parts[0]);
200 let column = normalize_ident(&parts[1]);
201 if inner.table_by_ref(&qualifier).is_some() {
202 Side::Inner(column)
203 } else if outer.table_by_ref(&qualifier).is_some() {
204 Side::Outer(column)
205 } else {
206 Side::Other
207 }
208 }
209 ast::Expr::Identifier(ident) => {
210 let column = normalize_ident(ident);
211 if inner.check_name(None, &column).is_ok() {
212 Side::Inner(column)
213 } else if outer.check_name(None, &column).is_ok() {
214 Side::Outer(column)
215 } else {
216 Side::Other
217 }
218 }
219 ast::Expr::Nested(nested) => classify(nested, inner, outer),
220 _ => Side::Other,
221 }
222}
223
224fn references_outer(expr: &ast::Expr, inner: &TableScope, outer: &TableScope) -> bool {
226 let walk = ast::visit_expressions(expr, |node| {
227 if matches!(classify(node, inner, outer), Side::Outer(_)) {
228 ControlFlow::Break(())
229 } else {
230 ControlFlow::Continue(())
231 }
232 });
233 walk.is_break()
234}
235
236fn unnest(expr: &ast::Expr) -> &ast::Expr {
237 match expr {
238 ast::Expr::Nested(inner) => unnest(inner),
239 other => other,
240 }
241}
242
243fn is_inequality(op: &ast::BinaryOperator) -> bool {
244 matches!(
245 op,
246 ast::BinaryOperator::Gt
247 | ast::BinaryOperator::GtEq
248 | ast::BinaryOperator::Lt
249 | ast::BinaryOperator::LtEq
250 | ast::BinaryOperator::NotEq
251 )
252}