1use alopex_core::kv::KVStore;
2use alopex_core::sql::subquery::{materialize_cache, nested_scan, semi_join_probe};
3
4use crate::catalog::Catalog;
5use crate::executor::evaluator::EvalContext;
6use crate::executor::{EvaluationError, ExecutorError, Result, Row};
7use crate::planner::logical_plan::LogicalPlan;
8use crate::planner::typed_expr::{Quantifier, TypedExpr, TypedExprKind};
9use crate::storage::{SqlTxn, SqlValue};
10
11pub fn execute_scalar_subquery<'txn, S: KVStore + 'txn, C: Catalog + ?Sized, T: SqlTxn<'txn, S>>(
13 txn: &mut T,
14 catalog: &C,
15 subquery: &LogicalPlan,
16) -> Result<SqlValue> {
17 execute_scalar_subquery_with_outer(txn, catalog, subquery, None)
18}
19
20pub(crate) fn execute_scalar_subquery_with_outer<
21 'txn,
22 S: KVStore + 'txn,
23 C: Catalog + ?Sized,
24 T: SqlTxn<'txn, S>,
25>(
26 txn: &mut T,
27 catalog: &C,
28 subquery: &LogicalPlan,
29 outer: Option<&Row>,
30) -> Result<SqlValue> {
31 let rows = execute_subquery_rows_with_outer(txn, catalog, subquery, outer)?;
32 if rows.len() > 1 {
33 return Err(ExecutorError::InvalidOperation {
34 operation: "execute_scalar_subquery".into(),
35 reason: "scalar subquery returned multiple rows".into(),
36 });
37 }
38 let Some(row) = rows.first() else {
39 return Ok(SqlValue::Null);
40 };
41 if row.len() != 1 {
42 return Err(ExecutorError::InvalidOperation {
43 operation: "execute_scalar_subquery".into(),
44 reason: format!("scalar subquery returned {} columns", row.len()),
45 });
46 }
47 Ok(row[0].clone())
48}
49
50pub fn execute_in_subquery<'txn, S: KVStore + 'txn, C: Catalog + ?Sized, T: SqlTxn<'txn, S>>(
57 txn: &mut T,
58 catalog: &C,
59 value: &SqlValue,
60 subquery: &LogicalPlan,
61 negated: bool,
62) -> Result<SqlValue> {
63 execute_in_subquery_with_outer(txn, catalog, value, subquery, negated, None)
64}
65
66pub(crate) fn execute_in_subquery_with_outer<
67 'txn,
68 S: KVStore + 'txn,
69 C: Catalog + ?Sized,
70 T: SqlTxn<'txn, S>,
71>(
72 txn: &mut T,
73 catalog: &C,
74 value: &SqlValue,
75 subquery: &LogicalPlan,
76 negated: bool,
77 outer: Option<&Row>,
78) -> Result<SqlValue> {
79 let rows = execute_subquery_rows_with_outer(txn, catalog, subquery, outer)?;
80 let mut unknown = false;
81 let matched = semi_join_probe(&rows, |row| {
82 let Some(candidate) = row.first() else {
83 return Ok(false);
84 };
85 if matches!(candidate, SqlValue::Null) || matches!(value, SqlValue::Null) {
86 unknown = true;
87 return Ok(false);
88 }
89 compare_values(
90 crate::ast::expr::BinaryOp::Eq,
91 value.clone(),
92 candidate.clone(),
93 )
94 })?;
95 if matched {
96 return Ok(SqlValue::Boolean(!negated));
97 }
98 if unknown {
99 return Ok(SqlValue::Null);
101 }
102 Ok(SqlValue::Boolean(negated))
103}
104
105pub fn execute_exists<'txn, S: KVStore + 'txn, C: Catalog + ?Sized, T: SqlTxn<'txn, S>>(
107 txn: &mut T,
108 catalog: &C,
109 subquery: &LogicalPlan,
110) -> Result<bool> {
111 execute_exists_with_outer(txn, catalog, subquery, false, None)
112}
113
114pub(crate) fn execute_exists_with_outer<
115 'txn,
116 S: KVStore + 'txn,
117 C: Catalog + ?Sized,
118 T: SqlTxn<'txn, S>,
119>(
120 txn: &mut T,
121 catalog: &C,
122 subquery: &LogicalPlan,
123 negated: bool,
124 outer: Option<&Row>,
125) -> Result<bool> {
126 let rows = execute_subquery_rows_with_outer(txn, catalog, subquery, outer)?;
127 let exists = semi_join_probe(&rows, |_| Ok::<bool, ExecutorError>(true))?;
128 Ok(if negated { !exists } else { exists })
129}
130
131pub(crate) fn evaluate_expr_with_subqueries<
132 'txn,
133 S: KVStore + 'txn,
134 C: Catalog + ?Sized,
135 T: SqlTxn<'txn, S>,
136>(
137 txn: &mut T,
138 catalog: &C,
139 expr: &TypedExpr,
140 row: &Row,
141) -> Result<SqlValue> {
142 match &expr.kind {
143 TypedExprKind::ScalarSubquery(subquery) => {
144 execute_scalar_subquery_with_outer(txn, catalog, subquery, Some(row))
145 }
146 TypedExprKind::InSubquery {
147 expr,
148 subquery,
149 negated,
150 } => {
151 let value = evaluate_expr_with_subqueries(txn, catalog, expr, row)?;
152 execute_in_subquery_with_outer(txn, catalog, &value, subquery, *negated, Some(row))
153 }
154 TypedExprKind::Exists { subquery, negated } => {
155 execute_exists_with_outer(txn, catalog, subquery, *negated, Some(row))
156 .map(SqlValue::Boolean)
157 }
158 TypedExprKind::Quantified {
159 expr,
160 op,
161 quantifier,
162 subquery,
163 } => {
164 let value = evaluate_expr_with_subqueries(txn, catalog, expr, row)?;
165 execute_quantified_with_outer(
166 txn,
167 catalog,
168 value,
169 *op,
170 *quantifier,
171 subquery,
172 Some(row),
173 )
174 .map(SqlValue::Boolean)
175 }
176 TypedExprKind::BinaryOp { left, op, right } if contains_subquery(expr) => {
177 let left = evaluate_expr_with_subqueries(txn, catalog, left, row)?;
178 let right = evaluate_expr_with_subqueries(txn, catalog, right, row)?;
179 crate::executor::evaluator::binary_op::eval_binary_values(op, left, right)
180 }
181 TypedExprKind::Case {
182 operand,
183 branches,
184 else_expr,
185 } if contains_subquery(expr) => {
186 let operand = operand
187 .as_deref()
188 .map(|operand| evaluate_expr_with_subqueries(txn, catalog, operand, row))
189 .transpose()?;
190 for branch in branches {
191 let matched = if let Some(operand) = &operand {
192 let condition = evaluate_expr_with_subqueries(txn, catalog, &branch.when, row)?;
193 crate::executor::evaluator::binary_op::eval_binary_values(
194 &crate::ast::expr::BinaryOp::Eq,
195 operand.clone(),
196 condition,
197 )?
198 } else {
199 evaluate_expr_with_subqueries(txn, catalog, &branch.when, row)?
200 };
201 if matches!(matched, SqlValue::Boolean(true)) {
202 return evaluate_expr_with_subqueries(txn, catalog, &branch.then, row);
203 }
204 }
205 if let Some(else_expr) = else_expr {
206 evaluate_expr_with_subqueries(txn, catalog, else_expr, row)
207 } else {
208 Ok(SqlValue::Null)
209 }
210 }
211 TypedExprKind::Cast {
212 expr: inner,
213 target_type,
214 } if contains_subquery(expr) => {
215 let value = evaluate_expr_with_subqueries(txn, catalog, inner, row)?;
216 crate::executor::evaluator::coerce_value(value, target_type)
217 }
218 TypedExprKind::TryCast {
219 expr: inner,
220 target_type,
221 } if contains_subquery(expr) => {
222 let value = evaluate_expr_with_subqueries(txn, catalog, inner, row)?;
223 crate::executor::evaluator::try_coerce_value(value, target_type)
224 }
225 _ => {
226 let ctx = EvalContext::new(&row.values);
227 crate::executor::evaluator::evaluate(expr, &ctx)
228 }
229 }
230}
231
232pub(crate) fn contains_subquery(expr: &TypedExpr) -> bool {
233 match &expr.kind {
234 TypedExprKind::ScalarSubquery(_)
235 | TypedExprKind::InSubquery { .. }
236 | TypedExprKind::Exists { .. }
237 | TypedExprKind::Quantified { .. } => true,
238 TypedExprKind::BinaryOp { left, right, .. } => {
239 contains_subquery(left) || contains_subquery(right)
240 }
241 TypedExprKind::UnaryOp { operand, .. } => contains_subquery(operand),
242 TypedExprKind::Case {
243 operand,
244 branches,
245 else_expr,
246 } => {
247 operand.as_deref().is_some_and(contains_subquery)
248 || branches.iter().any(|branch| {
249 contains_subquery(&branch.when) || contains_subquery(&branch.then)
250 })
251 || else_expr.as_deref().is_some_and(contains_subquery)
252 }
253 TypedExprKind::FunctionCall { args, .. } => args.iter().any(contains_subquery),
254 TypedExprKind::Cast { expr, .. }
255 | TypedExprKind::TryCast { expr, .. }
256 | TypedExprKind::IsNull { expr, .. } => contains_subquery(expr),
257 TypedExprKind::Between {
258 expr, low, high, ..
259 } => contains_subquery(expr) || contains_subquery(low) || contains_subquery(high),
260 TypedExprKind::Like {
261 expr,
262 pattern,
263 escape,
264 ..
265 } => {
266 contains_subquery(expr)
267 || contains_subquery(pattern)
268 || escape.as_deref().is_some_and(contains_subquery)
269 }
270 TypedExprKind::InList { expr, list, .. } => {
271 contains_subquery(expr) || list.iter().any(contains_subquery)
272 }
273 TypedExprKind::Literal(_)
274 | TypedExprKind::ColumnRef { .. }
275 | TypedExprKind::VectorLiteral(_) => false,
276 }
277}
278
279pub(crate) fn plan_contains_subquery(plan: &LogicalPlan) -> bool {
286 fn projection_contains_subquery(projection: &crate::planner::typed_expr::Projection) -> bool {
287 match projection {
288 crate::planner::typed_expr::Projection::All(_) => false,
289 crate::planner::typed_expr::Projection::Columns(cols) => {
290 cols.iter().any(|col| contains_subquery(&col.expr))
291 }
292 }
293 }
294
295 match plan {
296 LogicalPlan::Scan { projection, .. } => projection_contains_subquery(projection),
297 LogicalPlan::Values { rows, .. } => rows.iter().flatten().any(contains_subquery),
298 LogicalPlan::Filter { input, predicate } => {
299 contains_subquery(predicate) || plan_contains_subquery(input)
300 }
301 LogicalPlan::Project { input, projection } => {
302 projection_contains_subquery(projection) || plan_contains_subquery(input)
303 }
304 LogicalPlan::Join {
305 left,
306 right,
307 condition,
308 ..
309 }
310 | LogicalPlan::LateralJoin {
311 left,
312 right,
313 condition,
314 ..
315 } => {
316 condition.as_ref().is_some_and(contains_subquery)
317 || plan_contains_subquery(left)
318 || plan_contains_subquery(right)
319 }
320 LogicalPlan::TableFunction { args, .. } => args.iter().any(contains_subquery),
321 LogicalPlan::Aggregate {
322 input,
323 group_keys,
324 aggregates,
325 having,
326 projection,
327 grouping_sets: _,
328 } => {
329 group_keys.iter().any(contains_subquery)
330 || aggregates
331 .iter()
332 .any(|agg| agg.arg.as_ref().is_some_and(contains_subquery))
333 || having.as_ref().is_some_and(contains_subquery)
334 || projection_contains_subquery(projection)
335 || plan_contains_subquery(input)
336 }
337 LogicalPlan::SetOperation { left, right, .. } => {
338 plan_contains_subquery(left) || plan_contains_subquery(right)
339 }
340 LogicalPlan::RecursiveCte {
341 anchor,
342 recursive_term,
343 ..
344 } => plan_contains_subquery(anchor) || plan_contains_subquery(recursive_term),
345 LogicalPlan::RecursiveReference { .. } => false,
346 LogicalPlan::Sort { input, order_by } => {
347 order_by.iter().any(|sort| contains_subquery(&sort.expr))
348 || plan_contains_subquery(input)
349 }
350 LogicalPlan::DistinctOn {
351 input, order_by, ..
352 } => {
353 order_by.iter().any(|sort| contains_subquery(&sort.expr))
354 || plan_contains_subquery(input)
355 }
356 LogicalPlan::Limit { input, .. } => plan_contains_subquery(input),
357 _ => false,
359 }
360}
361
362fn execute_quantified_with_outer<
363 'txn,
364 S: KVStore + 'txn,
365 C: Catalog + ?Sized,
366 T: SqlTxn<'txn, S>,
367>(
368 txn: &mut T,
369 catalog: &C,
370 value: SqlValue,
371 op: crate::ast::expr::BinaryOp,
372 quantifier: Quantifier,
373 subquery: &LogicalPlan,
374 outer: Option<&Row>,
375) -> Result<bool> {
376 let rows = execute_subquery_rows_with_outer(txn, catalog, subquery, outer)?;
377 if rows.is_empty() {
378 return Ok(matches!(quantifier, Quantifier::All));
379 }
380 Ok(match quantifier {
381 Quantifier::Any => semi_join_probe(&rows, |row| {
382 let Some(candidate) = row.first() else {
383 return Ok::<bool, ExecutorError>(false);
384 };
385 compare_values(op, value.clone(), candidate.clone())
386 })?,
387 Quantifier::All => {
388 let has_non_match = semi_join_probe(&rows, |row| {
389 let Some(candidate) = row.first() else {
390 return Ok::<bool, ExecutorError>(false);
391 };
392 Ok(!compare_values(op, value.clone(), candidate.clone())?)
393 })?;
394 !has_non_match
395 }
396 })
397}
398
399fn execute_subquery_rows_with_outer<
400 'txn,
401 S: KVStore + 'txn,
402 C: Catalog + ?Sized,
403 T: SqlTxn<'txn, S>,
404>(
405 txn: &mut T,
406 catalog: &C,
407 subquery: &LogicalPlan,
408 outer: Option<&Row>,
409) -> Result<Vec<Vec<SqlValue>>> {
410 if outer.is_none() {
411 let mut cache = materialize_cache();
412 return cache.get_or_try_insert_with((), || {
413 nested_scan(|| {
414 super::execute_query_result_with_outer(txn, catalog, subquery.clone(), outer)
415 .map(|result| result.rows)
416 })
417 });
418 }
419
420 nested_scan(|| {
421 super::execute_query_result_with_outer(txn, catalog, subquery.clone(), outer)
422 .map(|result| result.rows)
423 })
424}
425
426fn compare_values(op: crate::ast::expr::BinaryOp, left: SqlValue, right: SqlValue) -> Result<bool> {
427 match crate::executor::evaluator::binary_op::eval_binary_values(&op, left, right)? {
428 SqlValue::Boolean(value) => Ok(value),
429 other => Err(ExecutorError::Evaluation(EvaluationError::TypeMismatch {
430 expected: "Boolean".into(),
431 actual: other.type_name().into(),
432 })),
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::plan_contains_subquery;
439 use crate::catalog::{Catalog, ColumnMetadata, MemoryCatalog, TableMetadata};
440 use crate::dialect::AlopexDialect;
441 use crate::parser::Parser;
442 use crate::planner::Planner;
443 use crate::planner::logical_plan::LogicalPlan;
444 use crate::planner::types::ResolvedType;
445
446 fn plan_select(sql: &str) -> LogicalPlan {
447 let mut catalog = MemoryCatalog::new();
448 catalog
449 .create_table(TableMetadata::new(
450 "users",
451 vec![
452 ColumnMetadata::new("id", ResolvedType::Integer),
453 ColumnMetadata::new("name", ResolvedType::Text),
454 ],
455 ))
456 .unwrap();
457 catalog
458 .create_table(TableMetadata::new(
459 "orders",
460 vec![
461 ColumnMetadata::new("id", ResolvedType::Integer),
462 ColumnMetadata::new("user_id", ResolvedType::Integer),
463 ColumnMetadata::new("total", ResolvedType::Integer),
464 ],
465 ))
466 .unwrap();
467 let statements = Parser::parse_sql(&AlopexDialect, sql).expect("parse sql");
468 assert_eq!(statements.len(), 1, "expected single statement");
469 Planner::new(&catalog)
470 .plan(&statements[0])
471 .expect("plan sql")
472 }
473
474 #[test]
475 fn detects_subquery_in_join_on_condition() {
476 let plan = plan_select(
477 "SELECT users.name FROM users JOIN orders ON users.id = orders.user_id AND orders.user_id IN (SELECT orders.user_id FROM orders)",
478 );
479 assert!(plan_contains_subquery(&plan));
480 }
481
482 #[test]
483 fn detects_subquery_in_having() {
484 let plan = plan_select(
485 "SELECT orders.user_id, COUNT(*) FROM orders GROUP BY orders.user_id HAVING COUNT(*) > (SELECT MIN(orders.total) FROM orders)",
486 );
487 assert!(plan_contains_subquery(&plan));
488 }
489
490 #[test]
491 fn detects_subquery_in_order_by() {
492 let plan = plan_select(
493 "SELECT users.name FROM users ORDER BY (SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id)",
494 );
495 assert!(plan_contains_subquery(&plan));
496 }
497
498 #[test]
499 fn plan_without_subquery_is_not_detected() {
500 let plan =
501 plan_select("SELECT users.name FROM users WHERE users.id = 1 ORDER BY users.name");
502 assert!(!plan_contains_subquery(&plan));
503 }
504}