Skip to main content

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 trigger context for OLD/NEW pseudo-variable resolution inside trigger bodies
45    /// (e.g. UPDATE…FROM in a trigger body referencing NEW.x / OLD.x — issue #5082)
46    pub trigger_context: Option<&'a crate::trigger_execution::TriggerContext<'a>>,
47    /// Optional CTE context for WITH clause results
48    pub cte_context: Option<&'a HashMap<String, CteResult>>,
49    /// Optional window function mapping
50    pub window_mapping: Option<&'a HashMap<crate::select::WindowFunctionKey, usize>>,
51}
52
53impl<'a> ExecutionContext<'a> {
54    /// Create a new execution context with required parameters.
55    ///
56    /// # Arguments
57    /// * `schema` - The combined schema for column resolution
58    /// * `database` - Database reference for table access
59    pub fn new(schema: &'a CombinedSchema, database: &'a vibesql_storage::Database) -> Self {
60        Self {
61            schema,
62            database,
63            outer_row: None,
64            outer_schema: None,
65            outer_rows: None,
66            procedural_context: None,
67            trigger_context: None,
68            cte_context: None,
69            window_mapping: None,
70        }
71    }
72
73    /// Add outer context for correlated subqueries.
74    ///
75    /// # Arguments
76    /// * `outer_row` - The current row from the outer query
77    /// * `outer_schema` - The schema of the outer query
78    #[must_use]
79    pub fn with_outer_context(
80        mut self,
81        outer_row: &'a vibesql_storage::Row,
82        outer_schema: &'a CombinedSchema,
83    ) -> Self {
84        self.outer_row = Some(outer_row);
85        self.outer_schema = Some(outer_schema);
86        self
87    }
88
89    /// Add all outer rows for outer-correlated aggregates (issue #4930).
90    ///
91    /// When an aggregate function in a scalar subquery references only outer columns,
92    /// it should aggregate over ALL outer rows, not just the current one. This enables
93    /// queries like `SELECT (SELECT string_agg(a1,'x') FROM t2) FROM t1` to work correctly.
94    ///
95    /// # Arguments
96    /// * `outer_rows` - All rows from the outer query
97    #[must_use]
98    pub fn with_outer_rows(mut self, outer_rows: &'a [vibesql_storage::Row]) -> Self {
99        self.outer_rows = Some(outer_rows);
100        self
101    }
102
103    /// Add procedural context for stored procedures/functions.
104    ///
105    /// # Arguments
106    /// * `proc_ctx` - The procedural execution context
107    #[must_use]
108    pub fn with_procedural_context(mut self, proc_ctx: &'a procedural::ExecutionContext) -> Self {
109        self.procedural_context = Some(proc_ctx);
110        self
111    }
112
113    /// Add trigger context for OLD/NEW pseudo-variable resolution.
114    ///
115    /// Used by the synthetic SELECT inside `UPDATE … FROM …` when running inside
116    /// a trigger body so that `OLD.col` / `NEW.col` resolve against the firing
117    /// row (issue #5082).
118    ///
119    /// # Arguments
120    /// * `trigger_ctx` - The trigger execution context
121    #[must_use]
122    pub fn with_trigger_context(
123        mut self,
124        trigger_ctx: &'a crate::trigger_execution::TriggerContext<'a>,
125    ) -> Self {
126        self.trigger_context = Some(trigger_ctx);
127        self
128    }
129
130    /// Add CTE context for WITH clause results.
131    ///
132    /// # Arguments
133    /// * `cte_ctx` - Map of CTE names to their results
134    #[must_use]
135    pub fn with_cte_context(mut self, cte_ctx: &'a HashMap<String, CteResult>) -> Self {
136        self.cte_context = Some(cte_ctx);
137        self
138    }
139
140    /// Add window function mapping.
141    ///
142    /// # Arguments
143    /// * `window_mapping` - Map of window function keys to result column indices
144    #[must_use]
145    pub fn with_window_mapping(
146        mut self,
147        window_mapping: &'a HashMap<crate::select::WindowFunctionKey, usize>,
148    ) -> Self {
149        self.window_mapping = Some(window_mapping);
150        self
151    }
152
153    /// Create a CombinedExpressionEvaluator with all the context from this struct.
154    ///
155    /// This replaces the many constructor variants like:
156    /// - `with_database()`
157    /// - `with_database_and_cte()`
158    /// - `with_database_and_outer_context()`
159    /// - `with_database_and_outer_context_and_cte()`
160    /// - `with_database_and_procedural_context()`
161    /// - `with_database_and_procedural_context_and_cte()`
162    /// - `with_database_and_windows()`
163    /// - `with_database_and_windows_and_cte()`
164    ///
165    /// By using a builder pattern, we consolidate all 8+ variants into a single method.
166    /// If outer_rows is set, it will be propagated to the evaluator for outer-correlated
167    /// aggregates (issue #4930).
168    pub fn create_evaluator(&self) -> CombinedExpressionEvaluator<'a> {
169        // Trigger context takes priority — used by the synthetic SELECT inside
170        // UPDATE…FROM running inside a trigger body so OLD/NEW resolve correctly
171        // (issue #5082). Trigger context is mutually exclusive with procedural
172        // context (a trigger body cannot run inside a stored procedure body), but
173        // we still allow it to compose with outer_rows, set below.
174        if let Some(trigger_ctx) = self.trigger_context {
175            let mut evaluator =
176                CombinedExpressionEvaluator::with_database_and_trigger_context(
177                    self.schema,
178                    self.database,
179                    trigger_ctx,
180                );
181            if let Some(outer_rows) = self.outer_rows {
182                evaluator.set_outer_rows(outer_rows);
183            }
184            return evaluator;
185        }
186
187        // Use the most complete constructor and set optional fields
188        // We match on the combination of optional fields to call the right constructor
189        // This is temporary until we refactor CombinedExpressionEvaluator itself
190        let mut evaluator = match (
191            self.outer_row,
192            self.outer_schema,
193            self.procedural_context,
194            self.cte_context,
195            self.window_mapping,
196        ) {
197            // With outer context and CTE
198            (Some(outer_row), Some(outer_schema), None, Some(cte_ctx), None) => {
199                CombinedExpressionEvaluator::with_database_and_outer_context_and_cte(
200                    self.schema,
201                    self.database,
202                    outer_row,
203                    outer_schema,
204                    cte_ctx,
205                )
206            }
207            // With outer context only
208            (Some(outer_row), Some(outer_schema), None, None, None) => {
209                CombinedExpressionEvaluator::with_database_and_outer_context(
210                    self.schema,
211                    self.database,
212                    outer_row,
213                    outer_schema,
214                )
215            }
216            // With procedural context and CTE
217            (None, None, Some(proc_ctx), Some(cte_ctx), None) => {
218                CombinedExpressionEvaluator::with_database_and_procedural_context_and_cte(
219                    self.schema,
220                    self.database,
221                    proc_ctx,
222                    cte_ctx,
223                )
224            }
225            // With procedural context only
226            (None, None, Some(proc_ctx), None, None) => {
227                CombinedExpressionEvaluator::with_database_and_procedural_context(
228                    self.schema,
229                    self.database,
230                    proc_ctx,
231                )
232            }
233            // With CTE context only
234            (None, None, None, Some(cte_ctx), None) => {
235                CombinedExpressionEvaluator::with_database_and_cte(
236                    self.schema,
237                    self.database,
238                    cte_ctx,
239                )
240            }
241            // With window mapping and CTE
242            (None, None, None, Some(cte_ctx), Some(window_map)) => {
243                CombinedExpressionEvaluator::with_database_and_windows_and_cte(
244                    self.schema,
245                    self.database,
246                    window_map,
247                    cte_ctx,
248                )
249            }
250            // With window mapping only
251            (None, None, None, None, Some(window_map)) => {
252                CombinedExpressionEvaluator::with_database_and_windows(
253                    self.schema,
254                    self.database,
255                    window_map,
256                )
257            }
258            // Base case: just database
259            (None, None, None, None, None) => {
260                CombinedExpressionEvaluator::with_database(self.schema, self.database)
261            }
262            // Unsupported combinations fall back to base
263            // Note: outer + procedural is not a valid combination in current usage
264            _ => CombinedExpressionEvaluator::with_database(self.schema, self.database),
265        };
266
267        // Set outer_rows if available (for outer-correlated aggregates, issue #4930)
268        if let Some(outer_rows) = self.outer_rows {
269            evaluator.set_outer_rows(outer_rows);
270        }
271
272        evaluator
273    }
274
275    /// Check if this context has outer context (for correlated subqueries).
276    #[inline]
277    pub fn has_outer_context(&self) -> bool {
278        self.outer_row.is_some() && self.outer_schema.is_some()
279    }
280
281    /// Check if this context has CTE context.
282    #[inline]
283    pub fn has_cte_context(&self) -> bool {
284        self.cte_context.is_some()
285    }
286
287    /// Check if this context has procedural context.
288    #[inline]
289    pub fn has_procedural_context(&self) -> bool {
290        self.procedural_context.is_some()
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use vibesql_catalog::TableSchema;
297
298    use super::*;
299    use crate::schema::CombinedSchema;
300
301    /// Builder for creating ExecutionContext from SelectExecutor fields.
302    ///
303    /// This helper extracts common context-building logic that's repeated
304    /// across multiple execution methods.
305    struct ExecutionContextBuilder<'a> {
306        schema: &'a CombinedSchema,
307        database: &'a vibesql_storage::Database,
308        outer_row: Option<&'a vibesql_storage::Row>,
309        outer_schema: Option<&'a CombinedSchema>,
310        procedural_context: Option<&'a procedural::ExecutionContext>,
311        cte_context: Option<&'a HashMap<String, CteResult>>,
312        executor_cte_context: Option<&'a HashMap<String, CteResult>>,
313    }
314
315    impl<'a> ExecutionContextBuilder<'a> {
316        /// Create a new builder with required parameters.
317        fn new(schema: &'a CombinedSchema, database: &'a vibesql_storage::Database) -> Self {
318            Self {
319                schema,
320                database,
321                outer_row: None,
322                outer_schema: None,
323                procedural_context: None,
324                cte_context: None,
325                executor_cte_context: None,
326            }
327        }
328
329        /// Set CTE context from query-level CTEs.
330        #[must_use]
331        fn cte_from_query(mut self, cte_results: &'a HashMap<String, CteResult>) -> Self {
332            if !cte_results.is_empty() {
333                self.cte_context = Some(cte_results);
334            }
335            self
336        }
337
338        /// Build the ExecutionContext.
339        ///
340        /// CTE context priority: query-level CTEs take precedence over executor-level.
341        fn build(self) -> ExecutionContext<'a> {
342            let mut ctx = ExecutionContext::new(self.schema, self.database);
343
344            // Set outer context if both row and schema are present
345            if let (Some(outer_row), Some(outer_schema)) = (self.outer_row, self.outer_schema) {
346                ctx = ctx.with_outer_context(outer_row, outer_schema);
347            }
348
349            // Set procedural context if present
350            if let Some(proc_ctx) = self.procedural_context {
351                ctx = ctx.with_procedural_context(proc_ctx);
352            }
353
354            // Set CTE context (query-level takes precedence)
355            if let Some(cte_ctx) = self.cte_context {
356                ctx = ctx.with_cte_context(cte_ctx);
357            } else if let Some(executor_cte) = self.executor_cte_context {
358                ctx = ctx.with_cte_context(executor_cte);
359            }
360
361            ctx
362        }
363    }
364
365    fn create_test_schema() -> CombinedSchema {
366        let table_schema = TableSchema::new("test".to_string(), vec![]);
367        CombinedSchema::from_table("test".to_string(), table_schema)
368    }
369
370    #[test]
371    fn test_execution_context_builder_basic() {
372        let schema = create_test_schema();
373        let database = vibesql_storage::Database::new();
374
375        let ctx = ExecutionContext::new(&schema, &database);
376
377        assert!(!ctx.has_outer_context());
378        assert!(!ctx.has_cte_context());
379        assert!(!ctx.has_procedural_context());
380    }
381
382    #[test]
383    fn test_execution_context_with_cte() {
384        let schema = create_test_schema();
385        let database = vibesql_storage::Database::new();
386        let cte_results: HashMap<String, CteResult> = HashMap::new();
387
388        let ctx = ExecutionContext::new(&schema, &database).with_cte_context(&cte_results);
389
390        assert!(ctx.has_cte_context());
391    }
392
393    #[test]
394    fn test_execution_context_builder_chain() {
395        let schema = create_test_schema();
396        let database = vibesql_storage::Database::new();
397        let cte_results: HashMap<String, CteResult> = HashMap::new();
398
399        let ctx =
400            ExecutionContextBuilder::new(&schema, &database).cte_from_query(&cte_results).build();
401
402        // Empty CTE map should not set context
403        assert!(!ctx.has_cte_context());
404    }
405}