vibesql_executor/select/executor/execute.rs
1//! Main execution methods for SelectExecutor
2//!
3//! This module implements the unified execution dispatcher that routes queries
4//! to the appropriate execution pipeline based on the selected strategy.
5//!
6//! ## Execution Pipeline Architecture
7//!
8//! The dispatcher uses the `ExecutionPipeline` trait to provide a unified interface
9//! for query execution across different strategies:
10//!
11//! - **NativeColumnar**: Zero-copy SIMD execution from columnar storage
12//! - **StandardColumnar**: SIMD execution with row-to-batch conversion
13//! - **RowOriented**: Traditional row-by-row execution
14//! - **ExpressionOnly**: SELECT without FROM clause (special case)
15//!
16//! ```text
17//! Strategy Selection → Create Pipeline → Execute Pipeline Stages → Results
18//! ↓
19//! apply_filter → apply_projection → apply_aggregation → apply_limit_offset
20//! ```
21
22use std::collections::HashMap;
23
24use super::builder::SelectExecutor;
25use crate::{
26 errors::ExecutorError,
27 optimizer::adaptive::{choose_execution_strategy, ExecutionStrategy, StrategyContext},
28 pipeline::{
29 ColumnarPipeline, ExecutionContext, ExecutionPipeline, NativeColumnarPipeline,
30 PipelineInput,
31 },
32 select::{
33 cte::{execute_ctes, execute_ctes_with_memory_check, CteResult},
34 helpers::apply_limit_offset,
35 join::FromResult,
36 SelectResult,
37 },
38};
39
40impl SelectExecutor<'_> {
41 /// Execute a SELECT statement
42 pub fn execute(
43 &self,
44 stmt: &vibesql_ast::SelectStmt,
45 ) -> Result<Vec<vibesql_storage::Row>, ExecutorError> {
46 // Validate aggregate function argument counts FIRST (issue #4367)
47 // This catches errors like max(), min(*), sum(*) before any execution
48 // Must happen before any fast paths or strategy selection
49 super::validation::validate_aggregate_arguments(&stmt.select_list)?;
50
51 // Validate no nested aggregates (issue #4439)
52 // This catches errors like SUM(MIN(x)) before any execution
53 // Uses "misuse of aggregate function X()" format to match SQLite's resolve.c
54 super::validation::validate_no_nested_aggregates(&stmt.select_list)?;
55
56 // Validate join table limit (issue #4711)
57 // SQLite enforces a limit of 64 tables in a single join
58 // This catches queries like SELECT * FROM t, t, t, ... (65+ times)
59 super::validation::validate_join_table_limit(stmt)?;
60
61 // Validate IN (SELECT ...) subquery column counts (issue #5191)
62 // SQLite raises "sub-select returns N columns - expected 1" at
63 // prepare time; the runtime check in the expression evaluator only
64 // fires when a row is actually evaluated, so it misses the
65 // empty-table case (window9.test 3.4).
66 super::validation::validate_in_subquery_column_counts(stmt, self.database)?;
67
68 // Validate SQLite index hints (issue #5235)
69 // INDEXED BY must name an index that exists on that table; SQLite
70 // reports "no such index: X" at prepare time. The hint is otherwise
71 // ignored by the planner (PR #5234).
72 super::validation::validate_index_hints(stmt, self.database)?;
73
74 // Validate aggregates with outer column references in subqueries (issue #4853)
75 // This catches the specific case where a scalar subquery appears as an argument
76 // to an outer aggregate function, and the subquery's aggregate references an
77 // outer column. Example: SELECT max((SELECT count(x) FROM t35b)) FROM t35a;
78 // Note: Standalone correlated subqueries are valid and allowed, e.g.,
79 // SELECT (SELECT count(x) FROM t35b) FROM t35a; is valid in SQLite.
80 super::validation::validate_aggregate_subquery_outer_refs(stmt, self.database)?;
81
82 // Validate bare scalar subqueries in the SELECT list / ORDER BY for
83 // misuse of aggregate / window / row-value (#5069, refined #5104).
84 //
85 // A "bare" scalar subquery (no FROM) re-borrows the outer aggregation
86 // context. SQLite has two distinct behaviors depending on where the
87 // subquery appears:
88 //
89 // * SELECT-list (#5104): `SELECT (SELECT avg(a)) FROM t2` is allowed —
90 // the outer query implicitly collapses to a single-row aggregate. We
91 // pass `SubqueryContext::SelectList` so the validator skips the
92 // "misuse of aggregate" rejection in this position.
93 // * WHERE / HAVING / ORDER BY (and arguments to outer functions): an
94 // outer-correlated aggregate inside a bare scalar subquery is still
95 // a misuse and must be rejected. We pass `WhereOrEqual` for ORDER BY
96 // to preserve SQLite's rejection there.
97 //
98 // Row-value misuse (`(SELECT (a, b))`) is detected in both contexts.
99 for item in &stmt.select_list {
100 if let vibesql_ast::SelectItem::Expression { expr, .. } = item {
101 super::validation::validate_subquery_context_misuse(
102 expr,
103 super::validation::SubqueryContext::SelectList,
104 )?;
105 }
106 }
107 if let Some(order_by) = stmt.order_by.as_deref() {
108 for item in order_by {
109 super::validation::validate_subquery_context_misuse(
110 &item.expr,
111 super::validation::SubqueryContext::WhereOrEqual,
112 )?;
113 }
114 }
115
116 // Validate GROUP BY clauses across this statement and all nested
117 // subqueries for window-function misuse, including positional
118 // (`GROUP BY 1`) and alias references that resolve to a window
119 // function in the subquery's SELECT list (#5093, window1.test 47.2).
120 // SQLite raises this at prepare time, so we must walk subqueries
121 // here rather than relying on per-SELECT execution paths (the
122 // outer WHERE may short-circuit before the inner SELECT executes).
123 super::validation::validate_group_by_window_misuse(stmt)?;
124
125 #[cfg(feature = "profile-q6")]
126 let execute_start = std::time::Instant::now();
127
128 // Reset arena and clear pointer-based caches for fresh query execution (only at top level)
129 // The subquery hash cache uses pointer-based keys which become invalid when ASTs are
130 // dropped and memory is reused, so we must clear it between queries.
131 // Note: The IN subquery result cache and correlation cache use content hashes as keys,
132 // not pointers, so they can safely persist across queries.
133 if self.subquery_depth == 0 {
134 self.reset_arena();
135 crate::evaluator::caching::clear_subquery_hash_cache();
136 }
137
138 // Check timeout before starting execution
139 self.check_timeout()?;
140
141 // Check subquery depth limit to prevent stack overflow
142 if self.subquery_depth >= crate::limits::MAX_EXPRESSION_DEPTH {
143 return Err(ExecutorError::ExpressionDepthExceeded {
144 depth: self.subquery_depth,
145 max_depth: crate::limits::MAX_EXPRESSION_DEPTH,
146 });
147 }
148
149 // Guard against exponentially self-referential view nests (SQLite ticket
150 // [d58ccbb3f1b], exercised by view3.test / #5394). VibeSQL materializes
151 // views lazily by executing their queries, so a doubling view nest
152 // (v2=v1∪v1, v4=v2∪v2, …) expands exponentially and would hang. Match
153 // SQLite by statically detecting >65535 references to any view before
154 // execution. Only run once per top-level query.
155 if self.subquery_depth == 0 {
156 crate::select::view_reference_guard::check_view_reference_limit(stmt, self.database)?;
157 }
158
159 // Fast path for simple point-lookup queries (TPC-C optimization)
160 // This bypasses expensive optimizer passes for queries like:
161 // SELECT col FROM table WHERE pk = value
162 // Skip fast path if reverse_unordered_selects is ON and no ORDER BY (needs reversal)
163 let skip_fast_path_for_reversal =
164 self.database.reverse_unordered_selects() && stmt.order_by.is_none();
165 if self.subquery_depth == 0
166 && self.outer_row.is_none()
167 && self.cte_context.is_none()
168 && !skip_fast_path_for_reversal
169 && super::fast_path::is_simple_point_query(stmt)
170 {
171 return self.execute_fast_path(stmt);
172 }
173
174 // Streaming aggregate fast path (#3815)
175 // For queries like: SELECT SUM(k) FROM sbtest1 WHERE id BETWEEN ? AND ?
176 // Accumulates aggregates inline during PK range scan without materializing rows
177 if self.subquery_depth == 0
178 && self.outer_row.is_none()
179 && self.cte_context.is_none()
180 && !skip_fast_path_for_reversal
181 && super::fast_path::is_streaming_aggregate_query(stmt)
182 {
183 if let Ok(result) = self.execute_streaming_aggregate(stmt) {
184 return Ok(result);
185 }
186 // Fall through to standard path if streaming aggregate fails
187 }
188
189 #[cfg(feature = "profile-q6")]
190 let _setup_time = execute_start.elapsed();
191
192 // Apply subquery rewriting optimizations (Phase 2 of IN subquery optimization)
193 // - Rewrites correlated IN → EXISTS with LIMIT 1 for early termination
194 // - Adds DISTINCT to uncorrelated IN subqueries to reduce duplicate processing
195 // This works in conjunction with Phase 1 (HashSet optimization, #2136)
196 #[cfg(feature = "profile-q6")]
197 let optimizer_start = std::time::Instant::now();
198
199 let optimized_stmt = crate::optimizer::rewrite_subquery_optimizations(stmt);
200
201 // Push WHERE conjuncts into window-function subqueries/views (#5292)
202 // Predicates over a PARTITION BY prefix of every window in a derived
203 // table or view are copied into the inner WHERE clause, filtering
204 // whole partitions before the window functions run (and enabling
205 // index scans inside the subquery). Mirrors SQLite's
206 // pushDownWhereTerms() gate for window queries.
207 //
208 // CTE names already in scope (from enclosing queries) are threaded
209 // in so the pass can decline view expansion for shadowed names: at
210 // execution time CTEs take precedence over catalog views, and this
211 // pass runs before CTE resolution. Empty set when there is no outer
212 // CTE context (the common case; HashSet::new() does not allocate).
213 let outer_cte_names: std::collections::HashSet<String> = self
214 .cte_context
215 .map(|ctx| ctx.keys().map(|name| name.to_ascii_lowercase()).collect())
216 .unwrap_or_default();
217 let optimized_stmt = crate::optimizer::push_where_into_window_subqueries(
218 optimized_stmt,
219 self.database,
220 &outer_cte_names,
221 );
222
223 // Apply scalar subquery decorrelation (#4760)
224 // Transforms correlated scalar subqueries with aggregates (e.g., AVG, SUM, MIN)
225 // into CTE + JOIN patterns for O(n) instead of O(n²) execution.
226 // Example: WHERE x > 1.2 * (SELECT AVG(y) FROM t WHERE t.c = outer.c)
227 // Becomes: WITH _cte AS (SELECT c, AVG(y) FROM t GROUP BY c)
228 // ... JOIN _cte ON outer.c = _cte.c WHERE x > 1.2 * _cte._avg
229 let optimized_stmt = crate::optimizer::apply_scalar_decorrelation(&optimized_stmt);
230
231 #[cfg(feature = "profile-q6")]
232 let _optimizer_time = optimizer_start.elapsed();
233
234 // Eliminate unused tables that create unnecessary cross joins (#3556)
235 // Must run BEFORE semi-join transformation to avoid complex interactions
236 // with derived tables from EXISTS/IN transformations
237 let optimized_stmt = crate::optimizer::eliminate_unused_tables(&optimized_stmt);
238
239 // Transform decorrelated IN/EXISTS subqueries to semi/anti-joins (#2424)
240 // This enables hash-based join execution instead of row-by-row subquery evaluation
241 // Converts WHERE clauses like "WHERE x IN (SELECT y FROM t)" to "SEMI JOIN t ON x = y"
242 let optimized_stmt = crate::optimizer::transform_subqueries_to_joins(&optimized_stmt);
243
244 // Execute CTEs if present and merge with outer query's CTE context
245 let mut cte_results = if let Some(with_clause) = &optimized_stmt.with_clause {
246 // This query has its own CTEs - execute them with memory tracking
247 execute_ctes_with_memory_check(
248 with_clause,
249 self.database,
250 |query, cte_ctx| self.execute_with_ctes(query, cte_ctx),
251 |size| self.track_memory_allocation(size),
252 )?
253 } else {
254 HashMap::new()
255 };
256
257 // If we have access to outer query's CTEs (for subqueries), merge them in
258 // Local CTEs take precedence over outer CTEs if there are name conflicts
259 if let Some(outer_cte_ctx) = self.cte_context {
260 for (name, result) in outer_cte_ctx {
261 cte_results.entry(name.clone()).or_insert_with(|| result.clone());
262 }
263 }
264
265 #[cfg(feature = "profile-q6")]
266 let _pre_execute_time = execute_start.elapsed();
267
268 // Execute the main query with CTE context
269 let mut result = self.execute_with_ctes(&optimized_stmt, &cte_results)?;
270
271 #[cfg(feature = "profile-q6")]
272 {
273 let _total_execute = execute_start.elapsed();
274 }
275
276 // Apply PRAGMA reverse_unordered_selects if enabled
277 // Only reverse if there's no ORDER BY clause in the original statement
278 if stmt.order_by.is_none() && self.database.reverse_unordered_selects() {
279 result.reverse();
280 }
281
282 Ok(result)
283 }
284
285 /// Execute a SELECT statement and return an iterator over results
286 ///
287 /// This enables early termination when the full result set is not needed,
288 /// such as for IN subqueries where we stop after finding the first match.
289 ///
290 /// # Phase 1 Implementation (Early Termination for IN subqueries)
291 ///
292 /// Current implementation materializes results then returns an iterator.
293 /// This still enables early termination in the consumer (e.g., eval_in_subquery)
294 /// by stopping iteration when a match is found.
295 ///
296 /// Future optimization: Leverage the existing RowIterator infrastructure
297 /// (crate::select::iterator) for truly lazy evaluation that stops execution
298 /// early, not just iteration.
299 pub fn execute_iter(
300 &self,
301 stmt: &vibesql_ast::SelectStmt,
302 ) -> Result<impl Iterator<Item = vibesql_storage::Row>, ExecutorError> {
303 // For Phase 1, materialize then return iterator
304 // This still enables early termination in the consumer
305 let rows = self.execute(stmt)?;
306 Ok(rows.into_iter())
307 }
308
309 /// Execute a SELECT statement using the fast path directly
310 ///
311 /// This method is used by prepared statements with cached SimpleFastPath plans.
312 /// It bypasses the `is_simple_point_query()` check because the eligibility was
313 /// already determined at prepare time.
314 ///
315 /// # Performance
316 ///
317 /// For repeated execution of prepared statements, this saves the cost of
318 /// re-checking fast path eligibility on every execution (~5-10µs per query).
319 pub fn execute_fast_path_with_columns(
320 &self,
321 stmt: &vibesql_ast::SelectStmt,
322 ) -> Result<SelectResult, ExecutorError> {
323 // Reset arena for fresh query execution
324 if self.subquery_depth == 0 {
325 self.reset_arena();
326 }
327
328 // Check timeout before starting execution
329 self.check_timeout()?;
330
331 // Execute via fast path directly (skip is_simple_point_query check)
332 let rows = self.execute_fast_path(stmt)?;
333
334 // Derive column names from the SELECT list
335 // For fast path queries, we don't have a FromResult, so pass None
336 // The column derivation will use the SELECT list expressions directly
337 let columns = self.derive_fast_path_column_names(stmt)?;
338
339 Ok(SelectResult { columns, rows })
340 }
341
342 /// Derive column names for fast path execution
343 ///
344 /// For fast path queries, we derive column names directly from the SELECT list
345 /// and table schema without going through the full FROM clause execution.
346 ///
347 /// # Performance Note (#3780)
348 ///
349 /// This method is called by `Session::execute_prepared()` to cache column names
350 /// in `SimpleFastPathPlan`. After the first execution, cached column names are
351 /// reused to avoid repeated table lookups and column name derivation.
352 pub fn derive_fast_path_column_names(
353 &self,
354 stmt: &vibesql_ast::SelectStmt,
355 ) -> Result<Vec<String>, ExecutorError> {
356 use vibesql_ast::{FromClause, SelectItem};
357
358 // Get table name and schema for column resolution
359 let (table_name, table_alias) = match &stmt.from {
360 Some(FromClause::Table { name, alias, .. }) => (name.as_str(), alias.as_deref()),
361 _ => {
362 return Err(ExecutorError::Other(
363 "Fast path requires simple table FROM clause".to_string(),
364 ))
365 }
366 };
367
368 let table = self
369 .database
370 .get_table(table_name)
371 .ok_or_else(|| ExecutorError::TableNotFound(table_name.to_string()))?;
372
373 let mut columns = Vec::with_capacity(stmt.select_list.len());
374
375 for item in &stmt.select_list {
376 match item {
377 SelectItem::Wildcard { .. } => {
378 // Add all columns from the table
379 for col in &table.schema.columns {
380 columns.push(col.name.clone());
381 }
382 }
383 SelectItem::QualifiedWildcard { qualifier, .. } => {
384 // Check if qualifier matches table name or alias
385 let effective_name = table_alias.unwrap_or(table_name);
386 if qualifier.eq_ignore_ascii_case(effective_name)
387 || qualifier.eq_ignore_ascii_case(table_name)
388 {
389 for col in &table.schema.columns {
390 columns.push(col.name.clone());
391 }
392 }
393 }
394 SelectItem::Expression { expr, alias: col_alias, .. } => {
395 // Use alias if provided, otherwise derive from expression
396 let col_name = if let Some(a) = col_alias {
397 a.clone()
398 } else {
399 self.derive_column_name_from_expr(expr)
400 };
401 columns.push(col_name);
402 }
403 }
404 }
405
406 Ok(columns)
407 }
408
409 /// Derive a column name from an expression
410 fn derive_column_name_from_expr(&self, expr: &vibesql_ast::Expression) -> String {
411 match expr {
412 vibesql_ast::Expression::ColumnRef(col_id) => col_id.column_canonical().to_string(),
413 vibesql_ast::Expression::Literal(val) => format!("{}", val),
414 vibesql_ast::Expression::Wildcard => "*".to_string(),
415 _ => "?column?".to_string(),
416 }
417 }
418
419 /// Resolve the output column names of a SELECT statement WITHOUT returning rows.
420 ///
421 /// This is the read-only column-name resolver used by the PostgreSQL extended
422 /// query protocol's `Describe` message (#5429), which must report the
423 /// `RowDescription` before any `Execute`. It reuses the exact wildcard
424 /// expansion and expression-naming logic as [`execute_with_columns`]
425 /// (`derive_column_names`), so the names match the simple-query path
426 /// label-for-label, including:
427 /// - explicit columns and aliases (`SELECT a, b AS x` -> `[a, x]`)
428 /// - `SELECT *` / `t.*` expanded against the catalog schema
429 /// - join wildcards (`SELECT t1.*, t2.c` -> t1's columns then `c`)
430 /// - derived names for expression columns
431 ///
432 /// Unlike `execute_with_columns`, this does NOT materialize result rows: it
433 /// builds the FROM-clause schema (the same `FromResult` the executor uses)
434 /// purely to expand wildcards, then derives the names.
435 pub fn resolve_column_names(
436 &self,
437 stmt: &vibesql_ast::SelectStmt,
438 ) -> Result<Vec<String>, ExecutorError> {
439 // Guard exponentially self-referential view nests before touching the
440 // FROM clause (which materializes views), mirroring execute_with_columns
441 // (#5394, view3.test).
442 crate::select::view_reference_guard::check_view_reference_limit(stmt, self.database)?;
443
444 // Resolve SELECT aliases in WHERE clause for schema construction, matching
445 // execute_with_columns so the FROM schema is built identically.
446 let resolved_where = if stmt.where_clause.is_some()
447 && crate::select::order::select_list_has_aliases(&stmt.select_list)
448 {
449 stmt.where_clause.as_ref().map(|where_expr| {
450 if let Some(from_clause) = &stmt.from {
451 if let Some(early_schema) =
452 super::aggregation::build_early_schema(from_clause, self.database)
453 {
454 return crate::select::order::resolve_where_aliases_with_schema(
455 where_expr,
456 &stmt.select_list,
457 &early_schema,
458 );
459 }
460 }
461 crate::select::order::resolve_where_aliases(where_expr, &stmt.select_list)
462 })
463 } else {
464 stmt.where_clause.clone()
465 };
466
467 // Build the FROM result to access the combined schema. This is required
468 // to expand `*` / `table.*` against the real column layout (including
469 // joins and NATURAL/USING deduplication). We do not return the rows.
470 let from_result = if let Some(from_clause) = &stmt.from {
471 let mut cte_results = if let Some(with_clause) = &stmt.with_clause {
472 execute_ctes(with_clause, self.database, |query, cte_ctx| {
473 self.execute_with_ctes(query, cte_ctx)
474 })?
475 } else {
476 HashMap::new()
477 };
478 if let Some(outer_cte_ctx) = self.cte_context {
479 for (name, result) in outer_cte_ctx {
480 cte_results.entry(name.clone()).or_insert_with(|| result.clone());
481 }
482 }
483 let limit_val = stmt
484 .limit
485 .as_ref()
486 .map(|expr| self.eval_limit_offset_expr(expr, "LIMIT"))
487 .transpose()?;
488 Some(self.execute_from_with_where(
489 from_clause,
490 &cte_results,
491 resolved_where.as_ref(),
492 stmt.order_by.as_deref(),
493 limit_val,
494 Some(&stmt.select_list),
495 )?)
496 } else {
497 None
498 };
499
500 // Derive column names exactly as execute_with_columns does.
501 let columns = if stmt.select_list.is_empty() {
502 if let Some(values_rows) = &stmt.values {
503 let num_cols = values_rows.first().map(|r| r.len()).unwrap_or(0);
504 (1..=num_cols).map(|i| format!("column{}", i)).collect()
505 } else {
506 Vec::new()
507 }
508 } else {
509 self.derive_column_names(&stmt.select_list, from_result.as_ref())?
510 };
511
512 Ok(columns)
513 }
514
515 /// Execute a SELECT statement and return both columns and rows
516 pub fn execute_with_columns(
517 &self,
518 stmt: &vibesql_ast::SelectStmt,
519 ) -> Result<SelectResult, ExecutorError> {
520 // Guard exponentially self-referential view nests before executing the
521 // FROM clause (which materializes views). This entry point runs the FROM
522 // *before* execute(), so the guard inside execute() would be too late
523 // (#5394, view3.test).
524 crate::select::view_reference_guard::check_view_reference_limit(stmt, self.database)?;
525
526 // Resolve SELECT aliases in WHERE clause BEFORE predicate pushdown (SQLite extension)
527 // This allows queries like: SELECT f1-22 AS x FROM t1 WHERE x > 0
528 // IMPORTANT: Use schema-aware resolution to avoid incorrectly substituting
529 // table column names with aggregate aliases (SQLite behavior)
530 // Example: SELECT COUNT(*) AS col1 FROM tab0 WHERE col1 > 0
531 // Here 'col1' in WHERE refers to the TABLE COLUMN, not the COUNT(*) alias
532 // PERFORMANCE: Skip alias resolution if no aliases exist (common case in OLTP)
533 let resolved_where = if stmt.where_clause.is_some()
534 && crate::select::order::select_list_has_aliases(&stmt.select_list)
535 {
536 stmt.where_clause.as_ref().map(|where_expr| {
537 // Try to build early schema from FROM clause
538 if let Some(from_clause) = &stmt.from {
539 if let Some(early_schema) =
540 super::aggregation::build_early_schema(from_clause, self.database)
541 {
542 // Use schema-aware resolution
543 return crate::select::order::resolve_where_aliases_with_schema(
544 where_expr,
545 &stmt.select_list,
546 &early_schema,
547 );
548 }
549 }
550 // Fall back to non-schema-aware resolution for complex FROM clauses
551 crate::select::order::resolve_where_aliases(where_expr, &stmt.select_list)
552 })
553 } else {
554 stmt.where_clause.clone()
555 };
556
557 // First, get the FROM result to access the schema
558 let from_result = if let Some(from_clause) = &stmt.from {
559 let mut cte_results = if let Some(with_clause) = &stmt.with_clause {
560 execute_ctes(with_clause, self.database, |query, cte_ctx| {
561 self.execute_with_ctes(query, cte_ctx)
562 })?
563 } else {
564 HashMap::new()
565 };
566 // If we have access to outer query's CTEs (for subqueries/derived tables), merge them
567 // in Local CTEs take precedence over outer CTEs if there are name conflicts
568 // This is critical for queries like TPC-DS Q2 where CTEs are referenced from derived
569 // tables
570 if let Some(outer_cte_ctx) = self.cte_context {
571 for (name, result) in outer_cte_ctx {
572 cte_results.entry(name.clone()).or_insert_with(|| result.clone());
573 }
574 }
575 // Pass WHERE, ORDER BY, and LIMIT for optimizations
576 // This is critical for GROUP BY queries to avoid CROSS JOINs
577 // LIMIT enables early termination when ORDER BY is satisfied by index (#3253)
578 // Pass select_list for table elimination optimization (#3556)
579 let limit_val = stmt
580 .limit
581 .as_ref()
582 .map(|expr| self.eval_limit_offset_expr(expr, "LIMIT"))
583 .transpose()?;
584 Some(self.execute_from_with_where(
585 from_clause,
586 &cte_results,
587 resolved_where.as_ref(),
588 stmt.order_by.as_deref(),
589 limit_val,
590 Some(&stmt.select_list),
591 )?)
592 } else {
593 None
594 };
595
596 // Derive column names from the SELECT list (with table prefix for display)
597 // Issue #4696: For VALUES statements, select_list is empty - derive from VALUES rows
598 let columns = if stmt.select_list.is_empty() {
599 if let Some(values_rows) = &stmt.values {
600 // Generate column names: column1, column2, etc.
601 let num_cols = values_rows.first().map(|r| r.len()).unwrap_or(0);
602 (1..=num_cols).map(|i| format!("column{}", i)).collect()
603 } else {
604 // Empty select_list and no VALUES - return empty columns
605 Vec::new()
606 }
607 } else {
608 self.derive_column_names(&stmt.select_list, from_result.as_ref())?
609 };
610
611 // Execute the query to get rows
612 let rows = self.execute(stmt)?;
613
614 Ok(SelectResult { columns, rows })
615 }
616
617 /// Execute SELECT statement and return results with simple column names
618 ///
619 /// This is similar to `execute_with_columns` but returns column names without
620 /// table prefixes. This is used for internal purposes like view creation
621 /// where the full table.column format would cause column lookup issues.
622 pub fn execute_with_simple_columns(
623 &self,
624 stmt: &vibesql_ast::SelectStmt,
625 ) -> Result<SelectResult, ExecutorError> {
626 // Guard exponentially self-referential view nests before executing the
627 // FROM clause (which materializes views). See execute_with_columns and
628 // #5394 (view3.test).
629 crate::select::view_reference_guard::check_view_reference_limit(stmt, self.database)?;
630
631 // Resolve SELECT aliases in WHERE clause BEFORE predicate pushdown (SQLite extension)
632 // This allows queries like: SELECT f1-22 AS x FROM t1 WHERE x > 0
633 // IMPORTANT: Use schema-aware resolution to avoid incorrectly substituting
634 // table column names with aggregate aliases (SQLite behavior)
635 // PERFORMANCE: Skip alias resolution if no aliases exist (common case in OLTP)
636 let resolved_where = if stmt.where_clause.is_some()
637 && crate::select::order::select_list_has_aliases(&stmt.select_list)
638 {
639 stmt.where_clause.as_ref().map(|where_expr| {
640 // Try to build early schema from FROM clause
641 if let Some(from_clause) = &stmt.from {
642 if let Some(early_schema) =
643 super::aggregation::build_early_schema(from_clause, self.database)
644 {
645 // Use schema-aware resolution
646 return crate::select::order::resolve_where_aliases_with_schema(
647 where_expr,
648 &stmt.select_list,
649 &early_schema,
650 );
651 }
652 }
653 // Fall back to non-schema-aware resolution for complex FROM clauses
654 crate::select::order::resolve_where_aliases(where_expr, &stmt.select_list)
655 })
656 } else {
657 stmt.where_clause.clone()
658 };
659
660 // Execute the FROM clause to get combined schema
661 let from_result = if let Some(from_clause) = &stmt.from {
662 let mut cte_results = if let Some(with_clause) = &stmt.with_clause {
663 execute_ctes(with_clause, self.database, |query, cte_ctx| {
664 self.execute_with_ctes(query, cte_ctx)
665 })?
666 } else {
667 HashMap::new()
668 };
669 // If we have access to outer query's CTEs (for subqueries/derived tables), merge them
670 if let Some(outer_cte_ctx) = self.cte_context {
671 for (name, result) in outer_cte_ctx {
672 cte_results.entry(name.clone()).or_insert_with(|| result.clone());
673 }
674 }
675 let limit_val = stmt
676 .limit
677 .as_ref()
678 .map(|expr| self.eval_limit_offset_expr(expr, "LIMIT"))
679 .transpose()?;
680 Some(self.execute_from_with_where(
681 from_clause,
682 &cte_results,
683 resolved_where.as_ref(),
684 stmt.order_by.as_deref(),
685 limit_val,
686 Some(&stmt.select_list),
687 )?)
688 } else {
689 None
690 };
691
692 // Derive column names from the SELECT list (without table prefix)
693 // Issue #4696: For VALUES statements, select_list is empty - derive from VALUES rows
694 let columns = if stmt.select_list.is_empty() {
695 if let Some(values_rows) = &stmt.values {
696 // Generate column names: column1, column2, etc.
697 let num_cols = values_rows.first().map(|r| r.len()).unwrap_or(0);
698 (1..=num_cols).map(|i| format!("column{}", i)).collect()
699 } else {
700 // Empty select_list and no VALUES - return empty columns
701 Vec::new()
702 }
703 } else {
704 self.derive_simple_column_names(&stmt.select_list, from_result.as_ref())?
705 };
706
707 // Execute the query to get rows
708 let rows = self.execute(stmt)?;
709
710 Ok(SelectResult { columns, rows })
711 }
712
713 /// Execute SELECT statement with CTE context
714 ///
715 /// Uses unified strategy selection to determine the optimal execution path:
716 /// - NativeColumnar: Zero-copy SIMD execution from columnar storage
717 /// - StandardColumnar: SIMD execution with row-to-batch conversion
718 /// - RowOriented: Traditional row-by-row execution
719 /// - ExpressionOnly: SELECT without FROM clause (special case)
720 ///
721 /// ## Pipeline-Based Execution (Phase 5)
722 ///
723 /// This method uses the `ExecutionPipeline` trait to provide a unified interface
724 /// for query execution. Each strategy creates an appropriate pipeline that
725 /// implements filter, projection, aggregation, and limit/offset operations.
726 ///
727 /// ```text
728 /// Strategy Selection → Create Pipeline → Execute via Trait Methods
729 /// ↓
730 /// NativeColumnar → NativeColumnarPipeline::apply_*()
731 /// StandardColumnar → ColumnarPipeline::apply_*()
732 /// RowOriented → RowOrientedPipeline::apply_*()
733 /// ExpressionOnly → Special case (no table scan)
734 /// ```
735 pub(super) fn execute_with_ctes(
736 &self,
737 stmt: &vibesql_ast::SelectStmt,
738 cte_results: &HashMap<String, CteResult>,
739 ) -> Result<Vec<vibesql_storage::Row>, ExecutorError> {
740 // Note: Aggregate argument validation is done in execute() at the entry point.
741 // See issue #4367.
742
743 #[cfg(feature = "profile-q6")]
744 let _execute_ctes_start = std::time::Instant::now();
745
746 // Check if native columnar is enabled via feature flag or env var
747 let native_columnar_enabled =
748 cfg!(feature = "native-columnar") || std::env::var("VIBESQL_NATIVE_COLUMNAR").is_ok();
749
750 // Use unified strategy selection for the execution path
751 let strategy_ctx = StrategyContext::new(stmt, cte_results, native_columnar_enabled);
752 let strategy = choose_execution_strategy(&strategy_ctx);
753
754 log::debug!(
755 "Execution strategy selected: {} (reason: {})",
756 strategy.name(),
757 strategy.score().reason
758 );
759
760 #[cfg(feature = "profile-q6")]
761 eprintln!(
762 "[PROFILE-Q6] Execution strategy: {} ({})",
763 strategy.name(),
764 strategy.score().reason
765 );
766
767 // Dispatch based on selected strategy using ExecutionPipeline trait
768 // Pipeline execution returns Option<Vec<Row>> - None means fallback needed
769 let mut results = match strategy {
770 ExecutionStrategy::NativeColumnar { .. } => {
771 // First try the optimized zero-copy native columnar path
772 // This uses ColumnarBatch::from_storage_columnar() for zero-copy conversion
773 // and executes filter+aggregate in a single pass without row materialization
774 if let Some(result) = self.try_native_columnar_execution(stmt, cte_results)? {
775 #[cfg(feature = "profile-q6")]
776 eprintln!("[PROFILE-Q6] Native columnar: zero-copy path succeeded");
777 result
778 } else {
779 // Fall back to pipeline-based execution if zero-copy path is not applicable
780 // (e.g., complex predicates, multiple tables, unsupported aggregates)
781 log::debug!("Native columnar: zero-copy path not applicable, trying pipeline");
782 match self.execute_via_pipeline(
783 stmt,
784 cte_results,
785 NativeColumnarPipeline::new,
786 "NativeColumnar",
787 )? {
788 Some(result) => result,
789 None => {
790 // Fall back to row-oriented if pipeline also fails
791 log::debug!("Native columnar runtime fallback to row-oriented");
792 #[cfg(feature = "profile-q6")]
793 eprintln!("[PROFILE-Q6] Native columnar fallback to row-oriented");
794 self.execute_row_oriented(stmt, cte_results)?
795 }
796 }
797 }
798 }
799
800 ExecutionStrategy::StandardColumnar { .. } => {
801 // StandardColumnar uses the pipeline-based execution path
802 // Note: We don't use try_native_columnar_execution here because row tables
803 // go through the pipeline which correctly handles all data types including dates.
804 // The native columnar zero-copy path has known limitations with certain date
805 // comparisons.
806 match self.execute_via_pipeline(
807 stmt,
808 cte_results,
809 ColumnarPipeline::new,
810 "StandardColumnar",
811 )? {
812 Some(result) => result,
813 None => {
814 log::debug!("Standard columnar runtime fallback to row-oriented");
815 #[cfg(feature = "profile-q6")]
816 eprintln!("[PROFILE-Q6] Standard columnar fallback to row-oriented");
817 self.execute_row_oriented(stmt, cte_results)?
818 }
819 }
820 }
821
822 ExecutionStrategy::RowOriented { .. } => {
823 // Row-oriented uses the traditional path which has full feature support
824 // The RowOrientedPipeline is used for simpler queries, but complex
825 // queries (with JOINs, window functions, DISTINCT, etc.) need the
826 // full execute_row_oriented implementation
827
828 // Phase 4: Try columnar join execution for multi-table JOIN queries (#2943)
829 // This provides 3-5x speedup for TPC-H Q3 style queries
830 let has_joins = stmt
831 .from
832 .as_ref()
833 .is_some_and(|f| matches!(f, vibesql_ast::FromClause::Join { .. }));
834 if has_joins {
835 if let Some(result) = self.try_columnar_join_execution(stmt, cte_results)? {
836 log::info!("Columnar join execution succeeded");
837 // Apply LIMIT/OFFSET to columnar join results (#3776)
838 // Skip if set_operation exists - it will be applied later
839 if stmt.set_operation.is_none() {
840 let limit_val = stmt
841 .limit
842 .as_ref()
843 .map(|expr| self.eval_limit_offset_expr(expr, "LIMIT"))
844 .transpose()?;
845 let offset_val = stmt
846 .offset
847 .as_ref()
848 .map(|expr| self.eval_limit_offset_expr(expr, "OFFSET"))
849 .transpose()?;
850 apply_limit_offset(result, limit_val, offset_val)
851 } else {
852 result
853 }
854 } else {
855 log::debug!(
856 "Columnar join execution not applicable, falling back to row-oriented"
857 );
858 self.execute_row_oriented(stmt, cte_results)?
859 }
860 } else {
861 self.execute_row_oriented(stmt, cte_results)?
862 }
863 }
864
865 ExecutionStrategy::ExpressionOnly { .. } => {
866 // SELECT without FROM - special case that doesn't use pipelines
867 // May still have aggregates (e.g., SELECT COUNT(*), SELECT MAX(1))
868 // Note: Do NOT use early return here - we need to fall through to set operations
869 // handling
870 self.execute_expression_only(stmt, cte_results)?
871 }
872 };
873
874 // Handle set operations (UNION, INTERSECT, EXCEPT)
875 // Process operations left-to-right to ensure correct associativity
876 if let Some(set_op) = &stmt.set_operation {
877 // Extract collations from the leftmost SELECT list for set operation comparisons
878 // Use schema-aware lookup to get column collations from CREATE TABLE definitions
879 let collations = Self::extract_collations_from_select_list_with_schema(
880 &stmt.select_list,
881 Some(self.database),
882 stmt.from.as_ref(),
883 );
884 // Issue #4602: Compute left column count from AST for schema-level validation
885 // This is needed when the left result set is empty (table has no rows)
886 // Issue #4922: Must propagate errors (not use .ok()) to catch column count mismatches
887 // in set operations like UNION/INTERSECT/EXCEPT
888 let left_col_count = super::nonagg::compute_select_list_column_count(
889 stmt,
890 self.database,
891 Some(cte_results),
892 )?;
893 let left_col_count = Some(left_col_count);
894 results = self.execute_set_operations(
895 results,
896 set_op,
897 cte_results,
898 &collations,
899 left_col_count,
900 )?;
901
902 // Apply ORDER BY after set operations (if specified)
903 // The UNION's default sort is overridden by explicit ORDER BY
904 if let Some(order_by) = &stmt.order_by {
905 // Collect aliases from all UNION branches for ORDER BY resolution
906 // Pass database to enable wildcard expansion using table schemas
907 let all_aliases = super::set_operations::collect_union_aliases(self.database, stmt);
908 // Pass column collations from the first SELECT for collation-aware sorting
909 results = self.sort_set_operation_results(
910 results,
911 order_by,
912 &stmt.select_list,
913 &all_aliases,
914 &collations,
915 )?;
916 }
917
918 // Apply LIMIT/OFFSET to the final result (after all set operations and ORDER BY)
919 // For queries WITHOUT set operations, LIMIT/OFFSET is already applied
920 // in execute_without_aggregation() or execute_with_aggregation()
921 let limit_val = stmt
922 .limit
923 .as_ref()
924 .map(|expr| self.eval_limit_offset_expr(expr, "LIMIT"))
925 .transpose()?;
926 let offset_val = stmt
927 .offset
928 .as_ref()
929 .map(|expr| self.eval_limit_offset_expr(expr, "OFFSET"))
930 .transpose()?;
931 results = apply_limit_offset(results, limit_val, offset_val);
932 }
933
934 Ok(results)
935 }
936
937 /// Execute SELECT without FROM clause (ExpressionOnly strategy)
938 ///
939 /// This is a special case that doesn't use the pipeline trait since there's
940 /// no table scan involved. Handles both simple expressions, aggregates,
941 /// and standalone VALUES statements.
942 fn execute_expression_only(
943 &self,
944 stmt: &vibesql_ast::SelectStmt,
945 cte_results: &HashMap<String, CteResult>,
946 ) -> Result<Vec<vibesql_storage::Row>, ExecutorError> {
947 // Handle standalone VALUES statements (Issue #4546)
948 // VALUES(1,2), (3,4) returns rows directly without a SELECT list
949 if let Some(values_rows) = &stmt.values {
950 // Issue #5036: Check if any VALUES row contains window functions
951 // If so, evaluate those rows via SELECT-without-FROM path which supports
952 // window function evaluation, rather than the plain VALUES evaluator
953 let has_windows = values_rows
954 .iter()
955 .any(|row| row.iter().any(crate::select::window::expression_has_window_function));
956
957 let mut results = if has_windows {
958 let mut all_rows = Vec::with_capacity(values_rows.len());
959 for row_exprs in values_rows {
960 let row_has_window =
961 row_exprs.iter().any(crate::select::window::expression_has_window_function);
962 if row_has_window {
963 // Construct a SELECT <expr1>, <expr2>, ... statement
964 // and execute via the window-aware path
965 let select_list: Vec<vibesql_ast::SelectItem> = row_exprs
966 .iter()
967 .enumerate()
968 .map(|(i, expr)| vibesql_ast::SelectItem::Expression {
969 expr: expr.clone(),
970 alias: Some(format!("column{}", i + 1)),
971 source_text: None,
972 })
973 .collect();
974 let temp_stmt = vibesql_ast::SelectStmt {
975 with_clause: None,
976 distinct: false,
977 select_list,
978 into_table: None,
979 into_variables: None,
980 from: None,
981 where_clause: None,
982 group_by: None,
983 having: None,
984 window_definitions: None,
985 order_by: None,
986 limit: None,
987 offset: None,
988 set_operation: None,
989 values: None,
990 };
991 let row_results =
992 self.execute_select_without_from(&temp_stmt, cte_results)?;
993 all_rows.extend(row_results);
994 } else {
995 // No window functions - evaluate normally
996 let schema = crate::schema::CombinedSchema::empty();
997 let empty_row = vibesql_storage::Row::new(vec![]);
998 let mut evaluator =
999 crate::evaluator::CombinedExpressionEvaluator::with_database(
1000 &schema,
1001 self.database,
1002 );
1003 // Thread CTE context so subqueries in VALUES rows can
1004 // reference names bound by an enclosing WITH (#5350)
1005 if !cte_results.is_empty() {
1006 evaluator = evaluator.with_cte_context(cte_results);
1007 } else if let Some(cte_ctx) = self.cte_context {
1008 evaluator = evaluator.with_cte_context(cte_ctx);
1009 }
1010 let mut values = Vec::with_capacity(row_exprs.len());
1011 for expr in row_exprs {
1012 let value = evaluator.eval(expr, &empty_row)?;
1013 values.push(value);
1014 }
1015 all_rows.push(vibesql_storage::Row::new(values));
1016 }
1017 }
1018 all_rows
1019 } else {
1020 // Thread CTE context so subqueries in VALUES rows can reference
1021 // names bound by an enclosing WITH clause (#5353); fall back to
1022 // the outer context when no local CTEs were materialized.
1023 let cte_ctx =
1024 if !cte_results.is_empty() { Some(cte_results) } else { self.cte_context };
1025 let from_result = crate::select::scan::values::execute_values(
1026 values_rows,
1027 "_values_",
1028 None,
1029 Some(self.database),
1030 cte_ctx,
1031 )?;
1032 from_result.into_rows()
1033 };
1034
1035 // Issue #4696: If there's a set_operation, don't apply ORDER BY or LIMIT/OFFSET here
1036 // Let the caller handle them after set operations are processed
1037 if stmt.set_operation.is_some() {
1038 return Ok(results);
1039 }
1040
1041 // Apply ORDER BY if specified
1042 if let Some(order_by) = &stmt.order_by {
1043 // For VALUES, column names are column1, column2, etc.
1044 // We need to map ORDER BY expressions to column indices
1045 results = self.apply_values_order_by(results, order_by, values_rows)?;
1046 }
1047
1048 // Apply LIMIT/OFFSET
1049 let limit_val = stmt
1050 .limit
1051 .as_ref()
1052 .map(|expr| self.eval_limit_offset_expr(expr, "LIMIT"))
1053 .transpose()?;
1054 let offset_val = stmt
1055 .offset
1056 .as_ref()
1057 .map(|expr| self.eval_limit_offset_expr(expr, "OFFSET"))
1058 .transpose()?;
1059 return Ok(apply_limit_offset(results, limit_val, offset_val));
1060 }
1061
1062 let has_aggregates = self.has_aggregates(&stmt.select_list) || stmt.having.is_some();
1063
1064 if has_aggregates {
1065 // Aggregates without FROM need the aggregation path
1066 self.execute_with_aggregation(stmt, cte_results)
1067 } else {
1068 // Simple expression evaluation (e.g., SELECT 1 + 1)
1069 self.execute_select_without_from(stmt, cte_results)
1070 }
1071 }
1072
1073 /// Apply ORDER BY to VALUES statement results
1074 fn apply_values_order_by(
1075 &self,
1076 mut rows: Vec<vibesql_storage::Row>,
1077 order_by: &[vibesql_ast::OrderByItem],
1078 values_rows: &[Vec<vibesql_ast::Expression>],
1079 ) -> Result<Vec<vibesql_storage::Row>, ExecutorError> {
1080 // For VALUES, we sort by column position (1-indexed)
1081 // or by column name (column1, column2, etc.)
1082 let num_cols = values_rows.first().map(|r| r.len()).unwrap_or(0);
1083
1084 rows.sort_by(|a, b| {
1085 for item in order_by {
1086 // Get the column index from the ORDER BY expression
1087 let col_idx = match &item.expr {
1088 vibesql_ast::Expression::Literal(vibesql_types::SqlValue::Integer(n)) => {
1089 // 1-indexed column position
1090 (*n as usize).saturating_sub(1)
1091 }
1092 vibesql_ast::Expression::ColumnRef(col_id) => {
1093 // column1, column2, etc.
1094 let col_name = col_id.column_canonical();
1095 if let Some(stripped) = col_name.strip_prefix("column") {
1096 stripped.parse::<usize>().unwrap_or(0).saturating_sub(1)
1097 } else {
1098 0
1099 }
1100 }
1101 _ => 0, // Default to first column for complex expressions
1102 };
1103
1104 if col_idx < num_cols {
1105 let cmp = a.values[col_idx].partial_cmp(&b.values[col_idx]);
1106 if let Some(ord) = cmp {
1107 let ord = match item.direction {
1108 vibesql_ast::OrderDirection::Asc => ord,
1109 vibesql_ast::OrderDirection::Desc => ord.reverse(),
1110 };
1111 if ord != std::cmp::Ordering::Equal {
1112 return ord;
1113 }
1114 }
1115 }
1116 }
1117 std::cmp::Ordering::Equal
1118 });
1119
1120 Ok(rows)
1121 }
1122
1123 /// Execute a query using the specified execution pipeline
1124 ///
1125 /// This method provides a unified interface for pipeline-based execution.
1126 /// It creates the pipeline, prepares input, and executes the pipeline stages.
1127 ///
1128 /// Returns `Ok(Some(results))` if the pipeline executed successfully,
1129 /// `Ok(None)` if the pipeline cannot handle the query (fallback needed),
1130 /// or `Err` if an error occurred.
1131 ///
1132 /// # Type Parameters
1133 ///
1134 /// * `P` - The pipeline type (must implement `ExecutionPipeline`)
1135 /// * `F` - Factory function to create the pipeline
1136 fn execute_via_pipeline<P, F>(
1137 &self,
1138 stmt: &vibesql_ast::SelectStmt,
1139 cte_results: &HashMap<String, CteResult>,
1140 create_pipeline: F,
1141 strategy_name: &str,
1142 ) -> Result<Option<Vec<vibesql_storage::Row>>, ExecutorError>
1143 where
1144 P: ExecutionPipeline,
1145 F: FnOnce() -> P,
1146 {
1147 #[cfg(feature = "profile-q6")]
1148 let start = std::time::Instant::now();
1149
1150 // Check query complexity - pipelines don't support all features
1151 let has_aggregates = self.has_aggregates(&stmt.select_list) || stmt.having.is_some();
1152 let has_group_by = stmt.group_by.is_some();
1153 let has_joins =
1154 stmt.from.as_ref().is_some_and(|f| matches!(f, vibesql_ast::FromClause::Join { .. }));
1155 let has_order_by = stmt.order_by.is_some();
1156 let has_distinct = stmt.distinct;
1157 let has_set_ops = stmt.set_operation.is_some();
1158 let has_window_funcs = self.has_window_functions(&stmt.select_list);
1159 let has_distinct_aggregates = self.has_distinct_aggregates(&stmt.select_list);
1160
1161 // Issue #5104: implicit-outer-aggregate-collapse requires the
1162 // aggregation pipeline. The columnar pipelines do not support this
1163 // semantic, so fall back to the row-oriented path which routes
1164 // through `execute_with_aggregation`.
1165 if !has_aggregates
1166 && !has_group_by
1167 && self.select_list_has_outer_aggregate_collapse(&stmt.select_list)
1168 {
1169 log::debug!(
1170 "{} pipeline: implicit-outer-aggregate-collapse — falling back to row-oriented",
1171 strategy_name
1172 );
1173 return Ok(None);
1174 }
1175
1176 // Create the pipeline
1177 let pipeline = create_pipeline();
1178
1179 // Check if the pipeline supports this query pattern
1180 if !pipeline.supports_query_pattern(has_aggregates, has_group_by, has_joins) {
1181 log::debug!(
1182 "{} pipeline doesn't support query pattern (agg={}, group_by={}, joins={})",
1183 strategy_name,
1184 has_aggregates,
1185 has_group_by,
1186 has_joins
1187 );
1188 return Ok(None);
1189 }
1190
1191 // For complex queries (ORDER BY, DISTINCT, window functions, set ops, DISTINCT aggregates),
1192 // fall back to full execution paths which have complete support
1193 if has_order_by
1194 || has_distinct
1195 || has_window_funcs
1196 || has_set_ops
1197 || has_distinct_aggregates
1198 {
1199 log::debug!(
1200 "{} pipeline doesn't support complex features (order_by={}, distinct={}, window={}, set_ops={}, distinct_agg={})",
1201 strategy_name,
1202 has_order_by,
1203 has_distinct,
1204 has_window_funcs,
1205 has_set_ops,
1206 has_distinct_aggregates
1207 );
1208 return Ok(None);
1209 }
1210
1211 // Must have a FROM clause for pipeline execution
1212 let from_clause = match &stmt.from {
1213 Some(from) => from,
1214 None => return Ok(None),
1215 };
1216
1217 // Execute FROM clause to get input data
1218 // Note: WHERE, ORDER BY, and LIMIT are handled by the pipeline, not here
1219 // Note: Table elimination requires WHERE clause, so pass None for select_list too
1220 let from_result = self.execute_from_with_where(
1221 from_clause,
1222 cte_results,
1223 None, // Pipeline will apply WHERE filter
1224 None, // ORDER BY handled separately
1225 None, // LIMIT applied after pipeline
1226 None, // No table elimination when WHERE is deferred
1227 )?;
1228
1229 // Build execution context
1230 let mut exec_ctx = ExecutionContext::new(&from_result.schema, self.database);
1231 // Add outer context for correlated subqueries (#2998)
1232 if let (Some(outer_row), Some(outer_schema)) = (self.outer_row, self.outer_schema) {
1233 exec_ctx = exec_ctx.with_outer_context(outer_row, outer_schema);
1234 } else if let Some(proc_ctx) = self.procedural_context {
1235 // Procedural variables in WHERE / SELECT (e.g. stored function bodies)
1236 exec_ctx = exec_ctx.with_procedural_context(proc_ctx);
1237 } else if let Some(trigger_ctx) = self.trigger_context {
1238 // Issue #5082: thread trigger context so OLD/NEW pseudo-vars resolve in
1239 // the synthetic SELECT used by UPDATE…FROM inside trigger bodies.
1240 exec_ctx = exec_ctx.with_trigger_context(trigger_ctx);
1241 }
1242 // Add CTE context if available
1243 if !cte_results.is_empty() {
1244 exec_ctx = exec_ctx.with_cte_context(cte_results);
1245 }
1246
1247 // Validate column references BEFORE processing
1248 super::validation::validate_select_columns_with_context(
1249 &stmt.select_list,
1250 stmt.where_clause.as_ref(),
1251 &from_result.schema,
1252 self.procedural_context,
1253 self.outer_schema,
1254 )?;
1255
1256 // Prepare input from FROM result
1257 let input = PipelineInput::from_rows_owned(from_result.data.into_rows());
1258
1259 // Execute pipeline stages with fallback on error
1260 // If any pipeline stage fails with UnsupportedFeature, fall back to row-oriented
1261
1262 // Resolve SELECT aliases in WHERE clause (SQLite extension)
1263 // This allows queries like: SELECT f1-22 AS x FROM t1 WHERE x > 0
1264 // IMPORTANT: Use schema-aware resolution to avoid incorrectly substituting
1265 // table column names with aggregate aliases (issue #4XXX)
1266 // Example: SELECT COUNT(*) AS col1 FROM tab0 WHERE col1 > 0
1267 // Here 'col1' in WHERE refers to the TABLE COLUMN, not the COUNT(*) alias
1268 // PERFORMANCE: Skip alias resolution if no aliases exist (common case in OLTP)
1269 let resolved_where = if crate::select::order::select_list_has_aliases(&stmt.select_list) {
1270 stmt.where_clause.as_ref().map(|where_expr| {
1271 crate::select::order::resolve_where_aliases_with_schema(
1272 where_expr,
1273 &stmt.select_list,
1274 &from_result.schema,
1275 )
1276 })
1277 } else {
1278 stmt.where_clause.clone()
1279 };
1280
1281 // Stage 1: Filter (WHERE clause)
1282 let filtered = match pipeline.apply_filter(input, resolved_where.as_ref(), &exec_ctx) {
1283 Ok(result) => result,
1284 Err(ExecutorError::UnsupportedFeature(_))
1285 | Err(ExecutorError::UnsupportedExpression(_)) => {
1286 log::debug!("{} pipeline filter failed, falling back", strategy_name);
1287 return Ok(None);
1288 }
1289 Err(e) => return Err(e),
1290 };
1291
1292 // Stage 2: Projection or Aggregation
1293 let result = if has_aggregates || has_group_by {
1294 // Execute aggregation (includes projection)
1295 // Get GROUP BY expressions if present (as slice)
1296 let group_by_slice: Option<&[vibesql_ast::Expression]> =
1297 stmt.group_by.as_ref().and_then(|g| g.as_simple()).map(|v| v.as_slice());
1298 match pipeline.apply_aggregation(
1299 filtered.into_input(),
1300 &stmt.select_list,
1301 group_by_slice,
1302 stmt.having.as_ref(),
1303 &exec_ctx,
1304 ) {
1305 Ok(result) => result,
1306 Err(ExecutorError::UnsupportedFeature(_))
1307 | Err(ExecutorError::UnsupportedExpression(_)) => {
1308 log::debug!("{} pipeline aggregation failed, falling back", strategy_name);
1309 return Ok(None);
1310 }
1311 Err(e) => return Err(e),
1312 }
1313 } else {
1314 // Execute projection only
1315 match pipeline.apply_projection(filtered.into_input(), &stmt.select_list, &exec_ctx) {
1316 Ok(result) => result,
1317 Err(ExecutorError::UnsupportedFeature(_))
1318 | Err(ExecutorError::UnsupportedExpression(_)) => {
1319 log::debug!("{} pipeline projection failed, falling back", strategy_name);
1320 return Ok(None);
1321 }
1322 Err(e) => return Err(e),
1323 }
1324 };
1325
1326 // Stage 3: Limit/Offset (evaluate expressions and convert to u64)
1327 let limit_usize = stmt
1328 .limit
1329 .as_ref()
1330 .map(|expr| self.eval_limit_offset_expr(expr, "LIMIT"))
1331 .transpose()?;
1332 let offset_usize = stmt
1333 .offset
1334 .as_ref()
1335 .map(|expr| self.eval_limit_offset_expr(expr, "OFFSET"))
1336 .transpose()?;
1337 let limit_u64 = limit_usize.map(|l| l as u64);
1338 let offset_u64 = offset_usize.map(|o| o as u64);
1339 let final_result = pipeline.apply_limit_offset(result, limit_u64, offset_u64)?;
1340
1341 #[cfg(feature = "profile-q6")]
1342 {
1343 eprintln!("[PROFILE-Q6] ✓ {} pipeline execution: {:?}", strategy_name, start.elapsed());
1344 }
1345
1346 log::debug!("✓ {} pipeline execution succeeded", strategy_name);
1347 Ok(Some(final_result))
1348 }
1349
1350 /// Check if the select list contains window functions
1351 fn has_window_functions(&self, select_list: &[vibesql_ast::SelectItem]) -> bool {
1352 select_list.iter().any(|item| {
1353 if let vibesql_ast::SelectItem::Expression { expr, .. } = item {
1354 self.expr_has_window_function(expr)
1355 } else {
1356 false
1357 }
1358 })
1359 }
1360
1361 /// Recursively check if an expression contains a window function
1362 #[allow(clippy::only_used_in_recursion)]
1363 fn expr_has_window_function(&self, expr: &vibesql_ast::Expression) -> bool {
1364 match expr {
1365 vibesql_ast::Expression::WindowFunction { .. } => true,
1366 vibesql_ast::Expression::BinaryOp { left, right, .. } => {
1367 self.expr_has_window_function(left) || self.expr_has_window_function(right)
1368 }
1369 vibesql_ast::Expression::UnaryOp { expr, .. } => self.expr_has_window_function(expr),
1370 vibesql_ast::Expression::Function { args, .. } => {
1371 args.iter().any(|arg| self.expr_has_window_function(arg))
1372 }
1373 vibesql_ast::Expression::Case { operand, when_clauses, else_result } => {
1374 operand.as_ref().is_some_and(|e| self.expr_has_window_function(e))
1375 || when_clauses.iter().any(|case_when| {
1376 case_when.conditions.iter().any(|c| self.expr_has_window_function(c))
1377 || self.expr_has_window_function(&case_when.result)
1378 })
1379 || else_result.as_ref().is_some_and(|e| self.expr_has_window_function(e))
1380 }
1381 _ => false,
1382 }
1383 }
1384
1385 /// Check if the select list contains any DISTINCT aggregates (e.g., COUNT(DISTINCT x))
1386 fn has_distinct_aggregates(&self, select_list: &[vibesql_ast::SelectItem]) -> bool {
1387 select_list.iter().any(|item| {
1388 if let vibesql_ast::SelectItem::Expression { expr, .. } = item {
1389 self.expr_has_distinct_aggregate(expr)
1390 } else {
1391 false
1392 }
1393 })
1394 }
1395
1396 /// Recursively check if an expression contains a DISTINCT aggregate
1397 #[allow(clippy::only_used_in_recursion)]
1398 fn expr_has_distinct_aggregate(&self, expr: &vibesql_ast::Expression) -> bool {
1399 match expr {
1400 vibesql_ast::Expression::AggregateFunction { distinct, .. } => *distinct,
1401 vibesql_ast::Expression::BinaryOp { left, right, .. } => {
1402 self.expr_has_distinct_aggregate(left) || self.expr_has_distinct_aggregate(right)
1403 }
1404 vibesql_ast::Expression::UnaryOp { expr, .. } => self.expr_has_distinct_aggregate(expr),
1405 vibesql_ast::Expression::Function { args, .. } => {
1406 args.iter().any(|arg| self.expr_has_distinct_aggregate(arg))
1407 }
1408 vibesql_ast::Expression::Case { operand, when_clauses, else_result } => {
1409 operand.as_ref().is_some_and(|e| self.expr_has_distinct_aggregate(e))
1410 || when_clauses.iter().any(|case_when| {
1411 case_when.conditions.iter().any(|c| self.expr_has_distinct_aggregate(c))
1412 || self.expr_has_distinct_aggregate(&case_when.result)
1413 })
1414 || else_result.as_ref().is_some_and(|e| self.expr_has_distinct_aggregate(e))
1415 }
1416 _ => false,
1417 }
1418 }
1419
1420 /// Execute using traditional row-oriented path
1421 ///
1422 /// This is the fallback path when columnar execution is not available or not beneficial.
1423 fn execute_row_oriented(
1424 &self,
1425 stmt: &vibesql_ast::SelectStmt,
1426 cte_results: &HashMap<String, CteResult>,
1427 ) -> Result<Vec<vibesql_storage::Row>, ExecutorError> {
1428 let has_aggregates = self.has_aggregates(&stmt.select_list) || stmt.having.is_some();
1429 let has_group_by = stmt.group_by.is_some();
1430
1431 // Issue #5104: SQLite's implicit-outer-aggregate-collapse semantics.
1432 // When the SELECT list contains a bare scalar subquery whose body has
1433 // an aggregate referencing an outer column, the outer query collapses
1434 // into a single-row aggregate (with the inner aggregate computed over
1435 // all outer rows). Route through the aggregation pipeline so the
1436 // single-group grand-total path runs and `outer_rows` is wired through
1437 // to the inner subquery.
1438 let has_implicit_collapse = !has_aggregates
1439 && !has_group_by
1440 && self.select_list_has_outer_aggregate_collapse(&stmt.select_list);
1441
1442 if has_aggregates || has_group_by || has_implicit_collapse {
1443 self.execute_with_aggregation(stmt, cte_results)
1444 } else if let Some(from_clause) = &stmt.from {
1445 // Re-enabled predicate pushdown for all queries (issue #1902)
1446 //
1447 // Previously, predicate pushdown was selectively disabled for multi-column IN clauses
1448 // because index optimization happened in execute_without_aggregation() on row indices
1449 // from the FROM result. When predicate pushdown filtered rows early, the indices no
1450 // longer matched the original table, causing incorrect results.
1451 //
1452 // Now that all index optimization has been moved to the scan level
1453 // (execute_index_scan), it happens BEFORE predicate pushdown, avoiding the
1454 // row-index mismatch problem. This allows predicate pushdown to work
1455 // correctly for all queries, improving performance.
1456 //
1457 // Fixes issues #1807, #1895, #1896, and #1902.
1458
1459 // Resolve SELECT aliases in WHERE clause BEFORE predicate pushdown (SQLite extension)
1460 // This allows queries like: SELECT f1-22 AS x FROM t1 WHERE x > 0
1461 // The alias 'x' is resolved to 'f1-22' so predicate pushdown can work correctly
1462 // IMPORTANT: Use schema-aware resolution to avoid incorrectly substituting
1463 // table column names with aggregate aliases (SQLite behavior)
1464 // PERFORMANCE: Skip alias resolution if no aliases exist (common case in OLTP)
1465 let resolved_where = if stmt.where_clause.is_some()
1466 && crate::select::order::select_list_has_aliases(&stmt.select_list)
1467 {
1468 stmt.where_clause.as_ref().map(|where_expr| {
1469 // Try to build early schema from FROM clause
1470 if let Some(early_schema) =
1471 super::aggregation::build_early_schema(from_clause, self.database)
1472 {
1473 // Use schema-aware resolution
1474 return crate::select::order::resolve_where_aliases_with_schema(
1475 where_expr,
1476 &stmt.select_list,
1477 &early_schema,
1478 );
1479 }
1480 // Fall back to non-schema-aware resolution for complex FROM clauses
1481 crate::select::order::resolve_where_aliases(where_expr, &stmt.select_list)
1482 })
1483 } else {
1484 stmt.where_clause.clone()
1485 };
1486
1487 // Pass WHERE, ORDER BY, and LIMIT to execute_from for optimization
1488 // LIMIT enables early termination when ORDER BY is satisfied by index (#3253)
1489 // Pass select_list for table elimination optimization (#3556)
1490 //
1491 // Don't pass ORDER BY if there's a set operation - it will be handled at the set
1492 // operation level
1493 let order_by_hint =
1494 if stmt.set_operation.is_some() { None } else { stmt.order_by.as_deref() };
1495 // Don't pass LIMIT hint for set operations - limit must be applied after combining
1496 // results This prevents early termination from incorrectly limiting the
1497 // left side of UNION queries
1498 let limit_val = if stmt.set_operation.is_some() {
1499 None
1500 } else {
1501 stmt.limit
1502 .as_ref()
1503 .map(|expr| self.eval_limit_offset_expr(expr, "LIMIT"))
1504 .transpose()?
1505 };
1506 let from_result = self.execute_from_with_where(
1507 from_clause,
1508 cte_results,
1509 resolved_where.as_ref(),
1510 order_by_hint,
1511 limit_val,
1512 Some(&stmt.select_list),
1513 )?;
1514
1515 // Validate column references BEFORE processing rows (issue #2654)
1516 // This ensures column errors are caught even when tables are empty
1517 // Pass procedural context to allow procedure variables in WHERE clause
1518 // Pass outer_schema for correlated subqueries (#2694)
1519 // Note: We validate with the resolved_where since that's what gets executed
1520 super::validation::validate_select_columns_with_context(
1521 &stmt.select_list,
1522 resolved_where.as_ref(),
1523 &from_result.schema,
1524 self.procedural_context,
1525 self.outer_schema,
1526 )?;
1527
1528 self.execute_without_aggregation(stmt, from_result, cte_results)
1529 } else {
1530 // SELECT without FROM - evaluate expressions as a single row
1531 self.execute_select_without_from(stmt, cte_results)
1532 }
1533 }
1534
1535 /// Execute a FROM clause with WHERE, ORDER BY, and LIMIT for optimization
1536 ///
1537 /// The LIMIT parameter enables early termination optimization (#3253):
1538 /// - When ORDER BY is satisfied by an index and no post-filter is needed, the index scan can
1539 /// stop after fetching LIMIT rows
1540 ///
1541 /// Note: Table elimination (#3556) is now handled at the optimizer level
1542 /// via crate::optimizer::eliminate_unused_tables(), which runs before
1543 /// semi-join transformation to avoid complex interactions.
1544 pub(super) fn execute_from_with_where(
1545 &self,
1546 from: &vibesql_ast::FromClause,
1547 cte_results: &HashMap<String, CteResult>,
1548 where_clause: Option<&vibesql_ast::Expression>,
1549 order_by: Option<&[vibesql_ast::OrderByItem]>,
1550 limit: Option<usize>,
1551 _select_list: Option<&[vibesql_ast::SelectItem]>, /* No longer used - optimization moved
1552 * to optimizer pass */
1553 ) -> Result<FromResult, ExecutorError> {
1554 use crate::select::scan::execute_from_clause;
1555
1556 let from_result = execute_from_clause(
1557 from,
1558 cte_results,
1559 self.database,
1560 where_clause,
1561 order_by,
1562 limit,
1563 self.outer_row,
1564 self.outer_schema,
1565 |query| {
1566 // For derived table subqueries, create a child executor with:
1567 // 1. CTE context (allows CTEs from outer WITH clause to be referenced)
1568 // 2. Outer context (allows correlated columns from outer queries to be resolved)
1569 // Critical for:
1570 // - TPC-DS Q2: CTEs in FROM subqueries
1571 // - select1-18.x: Nested correlated subqueries referencing outer columns
1572 if !cte_results.is_empty() {
1573 // FIX for select1-18.x: When both CTE and outer context exist,
1574 // we need to use new_with_outer_and_cte_and_depth to pass both
1575 if let (Some(outer_row), Some(outer_schema)) =
1576 (self.outer_row, self.outer_schema)
1577 {
1578 let child = SelectExecutor::new_with_outer_and_cte_and_depth(
1579 self.database,
1580 outer_row,
1581 outer_schema,
1582 cte_results,
1583 self.subquery_depth,
1584 );
1585 child.execute_with_columns(query)
1586 } else {
1587 let child = SelectExecutor::new_with_cte_and_depth(
1588 self.database,
1589 cte_results,
1590 self.subquery_depth,
1591 );
1592 child.execute_with_columns(query)
1593 }
1594 } else if let (Some(outer_row), Some(outer_schema)) =
1595 (self.outer_row, self.outer_schema)
1596 {
1597 // FIX for select1-18.x: Pass outer context for derived table subqueries
1598 // This enables column resolution from outer scopes in deeply nested queries
1599 let child = SelectExecutor::new_with_outer_context_and_depth(
1600 self.database,
1601 outer_row,
1602 outer_schema,
1603 self.subquery_depth,
1604 );
1605 child.execute_with_columns(query)
1606 } else {
1607 self.execute_with_columns(query)
1608 }
1609 },
1610 )?;
1611
1612 // NOTE: We DON'T merge outer schema with from_result.schema here because:
1613 // 1. from_result.rows only contain values from inner tables
1614 // 2. Outer columns are resolved via the evaluator's outer_row/outer_schema
1615 // 3. Merging would create schema/row mismatch (schema has outer cols, rows don't)
1616
1617 Ok(from_result)
1618 }
1619}