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}