Skip to main content

radixdb_executor/subquery/
mod.rs

1// Copyright 2026 RadixDB Contributors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Subquery Execution
16//!
17//! This module handles execution of subqueries including:
18//! - EXISTS subqueries
19//! - Scalar subqueries
20//! - IN subqueries
21
22use std::sync::Arc;
23
24use radixdb_core::CompactArc;
25use radixdb_core::SmartString;
26
27use radixdb_core::{Error, Result, Value, ValueMap, ValueSet};
28use radixdb_sql::ast::*;
29use radixdb_sql::token::TokenType;
30use radixdb_storage::mvcc::engine::MVCCEngine;
31use radixdb_storage::traits::{Engine, QueryResult};
32
33use super::context::{
34    cache_batch_aggregate, cache_batch_aggregate_info, cache_count_counter,
35    cache_exists_correlation, cache_exists_fetcher, cache_exists_index, cache_exists_pred_key,
36    cache_exists_predicate, cache_exists_schema, cache_in_subquery, cache_scalar_subquery,
37    cache_semi_join_arc, compute_semi_join_cache_key, extract_table_names_for_cache,
38    get_cached_batch_aggregate, get_cached_batch_aggregate_info, get_cached_count_counter,
39    get_cached_exists_correlation, get_cached_exists_fetcher, get_cached_exists_index,
40    get_cached_exists_pred_key, get_cached_exists_predicate, get_cached_exists_schema,
41    get_cached_in_subquery, get_cached_scalar_subquery, get_cached_semi_join,
42    BatchAggregateLookupInfo, ExecutionContext, ExistsCorrelationInfo,
43};
44use super::expr_converter::convert_ast_to_storage_expr;
45use super::expression::compute_expression_hash;
46use super::operator::{ColumnInfo, MaterializedOperator, Operator};
47use super::operators::hash_join::{HashJoinOperator, JoinSide, JoinType};
48use super::utils::{dummy_token, dummy_token_clone, value_to_expression};
49use crate::access::handle::QueryTableHandle;
50
51// ============================================================================
52// Constants
53// ============================================================================
54
55/// Maximum number of row IDs to check when verifying visibility for EXISTS/COUNT.
56/// We batch up to this many to balance between:
57/// - Wasted work if first row is visible (checking extra rows)
58/// - Overhead if most row IDs point to deleted rows (need multiple round trips)
59///
60/// A value of 10 provides reasonable tradeoff for typical workloads.
61const VISIBILITY_CHECK_BATCH_SIZE: usize = 10;
62
63// ============================================================================
64// Semi-Join Optimization for EXISTS Subqueries
65// ============================================================================
66
67// Result type for correlation extraction: (outer_col, outer_table, inner_col, remaining_predicate)
68type CorrelationExtraction = (String, Option<String>, String, Option<Arc<Expression>>);
69
70/// Information extracted from an EXISTS subquery for semi-join optimization.
71/// Example: EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.amount > 500)
72/// - outer_column: "u.id" (or "id" with outer table "u")
73/// - inner_column: "o.user_id" (or "user_id")
74/// - inner_table: "orders"
75/// - inner_alias: Some("o")
76/// - non_correlated_where: Some("o.amount > 500")
77#[derive(Debug)]
78pub struct SemiJoinInfo {
79    /// The outer column referenced in the correlation (e.g., "id" from "u.id")
80    pub outer_column: String,
81    /// The outer table alias if qualified (e.g., "u" from "u.id")
82    pub outer_table: Option<String>,
83    /// The inner column used in the correlation (e.g., "user_id" from "o.user_id")
84    pub inner_column: String,
85    /// The inner table name
86    pub inner_table: String,
87    /// The inner table alias if present
88    pub inner_alias: Option<String>,
89    /// Non-correlated part of the WHERE clause (filters only on inner table)
90    /// Uses Arc to avoid cloning expression trees during semi-join optimization
91    pub non_correlated_where: Option<Arc<Expression>>,
92    /// Whether this is NOT EXISTS
93    pub is_negated: bool,
94}
95
96/// Information needed for index-nested-loop EXISTS execution.
97///
98/// This is used for direct index probing instead of running a full subquery.
99#[derive(Debug, Clone)]
100struct IndexNestedLoopInfo {
101    outer_column: String,
102    outer_table: Option<String>,
103    inner_column: String,
104    inner_table: String,
105    #[allow(dead_code)]
106    additional_predicate: Option<Expression>,
107}
108
109mod correlation;
110mod rewrite;
111mod semi_join;
112#[cfg(test)]
113mod tests;
114
115/// Narrow composition contract for recursive SELECT execution. Query-local
116/// caches remain owned by [`ExecutionContext`]; the host provides only the
117/// engine and the surrounding SELECT entrypoint.
118pub trait SubqueryHost: Sync {
119    fn subquery_engine(&self) -> &Arc<MVCCEngine>;
120    fn subquery_open_table(&self, table_name: &str) -> Result<QueryTableHandle>;
121    fn subquery_execute_select(
122        &self,
123        statement: &SelectStatement,
124        context: &ExecutionContext,
125    ) -> Result<Box<dyn QueryResult>>;
126}
127
128/// Single owner for subquery rewriting, correlation analysis, cache use and
129/// semi/anti-join execution.
130pub struct SubqueryExecutor<'a, H: SubqueryHost + ?Sized> {
131    host: &'a H,
132}
133
134impl<'a, H: SubqueryHost + ?Sized> SubqueryExecutor<'a, H> {
135    fn new(host: &'a H) -> Self {
136        Self { host }
137    }
138}
139
140/// Internal call surface between subquery traversal and the SELECT owner.
141pub trait SubqueryExecutorExt: SubqueryHost {
142    fn process_where_subqueries(
143        &self,
144        expression: &Expression,
145        context: &ExecutionContext,
146    ) -> Result<Expression> {
147        SubqueryExecutor::new(self).process_where_subqueries(expression, context)
148    }
149
150    fn execute_exists_subquery(
151        &self,
152        statement: &SelectStatement,
153        context: &ExecutionContext,
154    ) -> Result<bool> {
155        SubqueryExecutor::new(self).execute_exists_subquery(statement, context)
156    }
157
158    fn try_process_select_subqueries(
159        &self,
160        columns: &[Expression],
161        context: &ExecutionContext,
162    ) -> Result<Option<Vec<Expression>>> {
163        SubqueryExecutor::new(self).try_process_select_subqueries(columns, context)
164    }
165
166    fn process_correlated_expression(
167        &self,
168        expression: &Expression,
169        context: &ExecutionContext,
170    ) -> Result<Expression> {
171        SubqueryExecutor::new(self).process_correlated_expression(expression, context)
172    }
173
174    fn process_correlated_where(
175        &self,
176        expression: &Expression,
177        context: &ExecutionContext,
178    ) -> Result<Expression> {
179        SubqueryExecutor::new(self).process_correlated_where(expression, context)
180    }
181
182    fn should_use_index_nested_loop_for_anti_join(
183        &self,
184        info: &SemiJoinInfo,
185        outer_limit: Option<i64>,
186    ) -> bool {
187        SubqueryExecutor::new(self).should_use_index_nested_loop_for_anti_join(info, outer_limit)
188    }
189
190    fn execute_semi_join_optimization(
191        &self,
192        info: &SemiJoinInfo,
193        context: &ExecutionContext,
194    ) -> Result<CompactArc<ValueSet>> {
195        SubqueryExecutor::new(self).execute_semi_join_optimization(info, context)
196    }
197
198    fn execute_anti_join(
199        &self,
200        info: &SemiJoinInfo,
201        outer_rows: CompactArc<Vec<radixdb_core::Row>>,
202        outer_columns: &[String],
203        context: &ExecutionContext,
204    ) -> Result<radixdb_core::RowVec> {
205        SubqueryExecutor::new(self).execute_anti_join(info, outer_rows, outer_columns, context)
206    }
207
208    fn try_optimize_exists_to_semi_join(
209        &self,
210        expression: &Expression,
211        context: &ExecutionContext,
212        outer_tables: &[String],
213        outer_limit: Option<i64>,
214    ) -> Result<Option<Expression>> {
215        SubqueryExecutor::new(self).try_optimize_exists_to_semi_join(
216            expression,
217            context,
218            outer_tables,
219            outer_limit,
220        )
221    }
222
223    fn try_optimize_in_to_semi_join(
224        &self,
225        expression: &Expression,
226        context: &ExecutionContext,
227        outer_tables: &[String],
228    ) -> Result<Option<Expression>> {
229        SubqueryExecutor::new(self).try_optimize_in_to_semi_join(expression, context, outer_tables)
230    }
231
232    fn has_subqueries(expression: &Expression) -> bool
233    where
234        Self: Sized,
235    {
236        SubqueryExecutor::<Self>::has_subqueries(expression)
237    }
238
239    fn has_correlated_subqueries(expression: &Expression) -> bool
240    where
241        Self: Sized,
242    {
243        SubqueryExecutor::<Self>::has_correlated_subqueries(expression)
244    }
245
246    fn has_correlated_select_subqueries(columns: &[Expression]) -> bool
247    where
248        Self: Sized,
249    {
250        SubqueryExecutor::<Self>::has_correlated_select_subqueries(columns)
251    }
252
253    fn is_subquery_correlated(statement: &SelectStatement) -> bool
254    where
255        Self: Sized,
256    {
257        SubqueryExecutor::<Self>::is_subquery_correlated(statement)
258    }
259
260    fn try_extract_not_exists_info(
261        expression: &Expression,
262        outer_tables: &[String],
263    ) -> Option<SemiJoinInfo>
264    where
265        Self: Sized,
266    {
267        SubqueryExecutor::<Self>::try_extract_not_exists_info(expression, outer_tables)
268    }
269
270    fn collect_outer_table_names(table: &Option<Box<Expression>>) -> Vec<String>
271    where
272        Self: Sized,
273    {
274        SubqueryExecutor::<Self>::collect_outer_table_names(table)
275    }
276}
277
278impl<T: SubqueryHost + ?Sized> SubqueryExecutorExt for T {}