vibesql_executor/pipeline/context.rs
1//! Execution Context
2//!
3//! This module provides the `ExecutionContext` struct that bundles all the context
4//! needed for query execution, eliminating the need for multiple evaluator constructor
5//! variants throughout the codebase.
6
7use std::collections::HashMap;
8
9use crate::{
10 evaluator::CombinedExpressionEvaluator, procedural, schema::CombinedSchema,
11 select::cte::CteResult,
12};
13
14/// Execution context that bundles all information needed for query execution.
15///
16/// This struct consolidates the various optional contexts (outer row, procedural,
17/// CTE, windows) that were previously passed separately, reducing the number of
18/// evaluator constructor variants needed.
19///
20/// # Example
21///
22/// ```text
23/// let ctx = ExecutionContext::new(schema, database)
24/// .with_cte_context(cte_results)
25/// .with_outer_context(outer_row, outer_schema);
26///
27/// let evaluator = ctx.create_evaluator();
28/// ```
29pub struct ExecutionContext<'a> {
30 /// The combined schema for column resolution
31 pub schema: &'a CombinedSchema,
32 /// Database reference for table access and subqueries
33 pub database: &'a vibesql_storage::Database,
34 /// Optional outer row for correlated subqueries
35 pub outer_row: Option<&'a vibesql_storage::Row>,
36 /// Optional outer schema for correlated subqueries
37 pub outer_schema: Option<&'a CombinedSchema>,
38 /// Optional all outer rows for outer-correlated aggregates (issue #4930)
39 /// When an aggregate in a scalar subquery references only outer columns,
40 /// it should aggregate over ALL outer rows, not just the current one.
41 pub outer_rows: Option<&'a [vibesql_storage::Row]>,
42 /// Optional procedural context for stored procedures/functions
43 pub procedural_context: Option<&'a procedural::ExecutionContext>,
44 /// Optional CTE context for WITH clause results
45 pub cte_context: Option<&'a HashMap<String, CteResult>>,
46 /// Optional window function mapping
47 pub window_mapping: Option<&'a HashMap<crate::select::WindowFunctionKey, usize>>,
48}
49
50impl<'a> ExecutionContext<'a> {
51 /// Create a new execution context with required parameters.
52 ///
53 /// # Arguments
54 /// * `schema` - The combined schema for column resolution
55 /// * `database` - Database reference for table access
56 pub fn new(schema: &'a CombinedSchema, database: &'a vibesql_storage::Database) -> Self {
57 Self {
58 schema,
59 database,
60 outer_row: None,
61 outer_schema: None,
62 outer_rows: None,
63 procedural_context: None,
64 cte_context: None,
65 window_mapping: None,
66 }
67 }
68
69 /// Add outer context for correlated subqueries.
70 ///
71 /// # Arguments
72 /// * `outer_row` - The current row from the outer query
73 /// * `outer_schema` - The schema of the outer query
74 #[must_use]
75 pub fn with_outer_context(
76 mut self,
77 outer_row: &'a vibesql_storage::Row,
78 outer_schema: &'a CombinedSchema,
79 ) -> Self {
80 self.outer_row = Some(outer_row);
81 self.outer_schema = Some(outer_schema);
82 self
83 }
84
85 /// Add all outer rows for outer-correlated aggregates (issue #4930).
86 ///
87 /// When an aggregate function in a scalar subquery references only outer columns,
88 /// it should aggregate over ALL outer rows, not just the current one. This enables
89 /// queries like `SELECT (SELECT string_agg(a1,'x') FROM t2) FROM t1` to work correctly.
90 ///
91 /// # Arguments
92 /// * `outer_rows` - All rows from the outer query
93 #[must_use]
94 pub fn with_outer_rows(mut self, outer_rows: &'a [vibesql_storage::Row]) -> Self {
95 self.outer_rows = Some(outer_rows);
96 self
97 }
98
99 /// Add procedural context for stored procedures/functions.
100 ///
101 /// # Arguments
102 /// * `proc_ctx` - The procedural execution context
103 #[must_use]
104 pub fn with_procedural_context(mut self, proc_ctx: &'a procedural::ExecutionContext) -> Self {
105 self.procedural_context = Some(proc_ctx);
106 self
107 }
108
109 /// Add CTE context for WITH clause results.
110 ///
111 /// # Arguments
112 /// * `cte_ctx` - Map of CTE names to their results
113 #[must_use]
114 pub fn with_cte_context(mut self, cte_ctx: &'a HashMap<String, CteResult>) -> Self {
115 self.cte_context = Some(cte_ctx);
116 self
117 }
118
119 /// Add window function mapping.
120 ///
121 /// # Arguments
122 /// * `window_mapping` - Map of window function keys to result column indices
123 #[must_use]
124 pub fn with_window_mapping(
125 mut self,
126 window_mapping: &'a HashMap<crate::select::WindowFunctionKey, usize>,
127 ) -> Self {
128 self.window_mapping = Some(window_mapping);
129 self
130 }
131
132 /// Create a CombinedExpressionEvaluator with all the context from this struct.
133 ///
134 /// This replaces the many constructor variants like:
135 /// - `with_database()`
136 /// - `with_database_and_cte()`
137 /// - `with_database_and_outer_context()`
138 /// - `with_database_and_outer_context_and_cte()`
139 /// - `with_database_and_procedural_context()`
140 /// - `with_database_and_procedural_context_and_cte()`
141 /// - `with_database_and_windows()`
142 /// - `with_database_and_windows_and_cte()`
143 ///
144 /// By using a builder pattern, we consolidate all 8+ variants into a single method.
145 /// If outer_rows is set, it will be propagated to the evaluator for outer-correlated
146 /// aggregates (issue #4930).
147 pub fn create_evaluator(&self) -> CombinedExpressionEvaluator<'a> {
148 // Use the most complete constructor and set optional fields
149 // We match on the combination of optional fields to call the right constructor
150 // This is temporary until we refactor CombinedExpressionEvaluator itself
151 let mut evaluator = match (
152 self.outer_row,
153 self.outer_schema,
154 self.procedural_context,
155 self.cte_context,
156 self.window_mapping,
157 ) {
158 // With outer context and CTE
159 (Some(outer_row), Some(outer_schema), None, Some(cte_ctx), None) => {
160 CombinedExpressionEvaluator::with_database_and_outer_context_and_cte(
161 self.schema,
162 self.database,
163 outer_row,
164 outer_schema,
165 cte_ctx,
166 )
167 }
168 // With outer context only
169 (Some(outer_row), Some(outer_schema), None, None, None) => {
170 CombinedExpressionEvaluator::with_database_and_outer_context(
171 self.schema,
172 self.database,
173 outer_row,
174 outer_schema,
175 )
176 }
177 // With procedural context and CTE
178 (None, None, Some(proc_ctx), Some(cte_ctx), None) => {
179 CombinedExpressionEvaluator::with_database_and_procedural_context_and_cte(
180 self.schema,
181 self.database,
182 proc_ctx,
183 cte_ctx,
184 )
185 }
186 // With procedural context only
187 (None, None, Some(proc_ctx), None, None) => {
188 CombinedExpressionEvaluator::with_database_and_procedural_context(
189 self.schema,
190 self.database,
191 proc_ctx,
192 )
193 }
194 // With CTE context only
195 (None, None, None, Some(cte_ctx), None) => {
196 CombinedExpressionEvaluator::with_database_and_cte(
197 self.schema,
198 self.database,
199 cte_ctx,
200 )
201 }
202 // With window mapping and CTE
203 (None, None, None, Some(cte_ctx), Some(window_map)) => {
204 CombinedExpressionEvaluator::with_database_and_windows_and_cte(
205 self.schema,
206 self.database,
207 window_map,
208 cte_ctx,
209 )
210 }
211 // With window mapping only
212 (None, None, None, None, Some(window_map)) => {
213 CombinedExpressionEvaluator::with_database_and_windows(
214 self.schema,
215 self.database,
216 window_map,
217 )
218 }
219 // Base case: just database
220 (None, None, None, None, None) => {
221 CombinedExpressionEvaluator::with_database(self.schema, self.database)
222 }
223 // Unsupported combinations fall back to base
224 // Note: outer + procedural is not a valid combination in current usage
225 _ => CombinedExpressionEvaluator::with_database(self.schema, self.database),
226 };
227
228 // Set outer_rows if available (for outer-correlated aggregates, issue #4930)
229 if let Some(outer_rows) = self.outer_rows {
230 evaluator.set_outer_rows(outer_rows);
231 }
232
233 evaluator
234 }
235
236 /// Check if this context has outer context (for correlated subqueries).
237 #[inline]
238 pub fn has_outer_context(&self) -> bool {
239 self.outer_row.is_some() && self.outer_schema.is_some()
240 }
241
242 /// Check if this context has CTE context.
243 #[inline]
244 pub fn has_cte_context(&self) -> bool {
245 self.cte_context.is_some()
246 }
247
248 /// Check if this context has procedural context.
249 #[inline]
250 pub fn has_procedural_context(&self) -> bool {
251 self.procedural_context.is_some()
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use vibesql_catalog::TableSchema;
258
259 use super::*;
260 use crate::schema::CombinedSchema;
261
262 /// Builder for creating ExecutionContext from SelectExecutor fields.
263 ///
264 /// This helper extracts common context-building logic that's repeated
265 /// across multiple execution methods.
266 struct ExecutionContextBuilder<'a> {
267 schema: &'a CombinedSchema,
268 database: &'a vibesql_storage::Database,
269 outer_row: Option<&'a vibesql_storage::Row>,
270 outer_schema: Option<&'a CombinedSchema>,
271 procedural_context: Option<&'a procedural::ExecutionContext>,
272 cte_context: Option<&'a HashMap<String, CteResult>>,
273 executor_cte_context: Option<&'a HashMap<String, CteResult>>,
274 }
275
276 impl<'a> ExecutionContextBuilder<'a> {
277 /// Create a new builder with required parameters.
278 fn new(schema: &'a CombinedSchema, database: &'a vibesql_storage::Database) -> Self {
279 Self {
280 schema,
281 database,
282 outer_row: None,
283 outer_schema: None,
284 procedural_context: None,
285 cte_context: None,
286 executor_cte_context: None,
287 }
288 }
289
290 /// Set CTE context from query-level CTEs.
291 #[must_use]
292 fn cte_from_query(mut self, cte_results: &'a HashMap<String, CteResult>) -> Self {
293 if !cte_results.is_empty() {
294 self.cte_context = Some(cte_results);
295 }
296 self
297 }
298
299 /// Build the ExecutionContext.
300 ///
301 /// CTE context priority: query-level CTEs take precedence over executor-level.
302 fn build(self) -> ExecutionContext<'a> {
303 let mut ctx = ExecutionContext::new(self.schema, self.database);
304
305 // Set outer context if both row and schema are present
306 if let (Some(outer_row), Some(outer_schema)) = (self.outer_row, self.outer_schema) {
307 ctx = ctx.with_outer_context(outer_row, outer_schema);
308 }
309
310 // Set procedural context if present
311 if let Some(proc_ctx) = self.procedural_context {
312 ctx = ctx.with_procedural_context(proc_ctx);
313 }
314
315 // Set CTE context (query-level takes precedence)
316 if let Some(cte_ctx) = self.cte_context {
317 ctx = ctx.with_cte_context(cte_ctx);
318 } else if let Some(executor_cte) = self.executor_cte_context {
319 ctx = ctx.with_cte_context(executor_cte);
320 }
321
322 ctx
323 }
324 }
325
326 fn create_test_schema() -> CombinedSchema {
327 let table_schema = TableSchema::new("test".to_string(), vec![]);
328 CombinedSchema::from_table("test".to_string(), table_schema)
329 }
330
331 #[test]
332 fn test_execution_context_builder_basic() {
333 let schema = create_test_schema();
334 let database = vibesql_storage::Database::new();
335
336 let ctx = ExecutionContext::new(&schema, &database);
337
338 assert!(!ctx.has_outer_context());
339 assert!(!ctx.has_cte_context());
340 assert!(!ctx.has_procedural_context());
341 }
342
343 #[test]
344 fn test_execution_context_with_cte() {
345 let schema = create_test_schema();
346 let database = vibesql_storage::Database::new();
347 let cte_results: HashMap<String, CteResult> = HashMap::new();
348
349 let ctx = ExecutionContext::new(&schema, &database).with_cte_context(&cte_results);
350
351 assert!(ctx.has_cte_context());
352 }
353
354 #[test]
355 fn test_execution_context_builder_chain() {
356 let schema = create_test_schema();
357 let database = vibesql_storage::Database::new();
358 let cte_results: HashMap<String, CteResult> = HashMap::new();
359
360 let ctx =
361 ExecutionContextBuilder::new(&schema, &database).cte_from_query(&cte_results).build();
362
363 // Empty CTE map should not set context
364 assert!(!ctx.has_cte_context());
365 }
366}