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}