Skip to main content

alopex_sql/executor/query/
subquery.rs

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
11/// Execute scalar subquery.
12pub 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
50/// Execute IN subquery.
51///
52/// Implements SQL three-valued logic: when no row matches but the subquery
53/// result contains NULL (or the probe value itself is NULL and the result is
54/// non-empty), the comparison is UNKNOWN and `SqlValue::Null` is returned.
55/// Consequently `NOT IN` over a NULL-containing result never yields TRUE.
56pub 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        // UNKNOWN stays UNKNOWN under NOT.
100        return Ok(SqlValue::Null);
101    }
102    Ok(SqlValue::Boolean(negated))
103}
104
105/// Execute EXISTS subquery.
106pub 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
279/// Returns true if any expression in the query plan contains a subquery.
280///
281/// The streaming pipeline cannot evaluate subqueries because subquery
282/// execution needs transaction access, which the streaming iterators borrow
283/// exclusively. Plans containing subqueries must therefore be routed to the
284/// materializing execution path (see `build_streaming_pipeline_with_policy`).
285pub(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        // DML/DDL plans are never executed through the query pipelines.
358        _ => 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}