Skip to main content

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}