Skip to main content

vibesql_executor/select/
cte.rs

1//! Common Table Expression (CTE) handling for SELECT queries
2
3use std::{
4    collections::{HashMap, HashSet},
5    sync::Arc,
6};
7
8use crate::errors::ExecutorError;
9
10/// CTE result: (schema, shared rows)
11///
12/// Uses `Arc<Vec<Row>>` to enable O(1) cloning when CTEs are:
13/// - Propagated from outer queries to subqueries
14/// - Referenced multiple times without filtering
15///
16/// This avoids deep-cloning all rows on every CTE reference.
17pub type CteResult = (vibesql_catalog::TableSchema, Arc<Vec<vibesql_storage::Row>>);
18
19/// Execute all CTEs and return their results
20///
21/// CTEs are executed in order, allowing later CTEs to reference earlier ones.
22///
23/// The `database` reference is used to statically expand wildcard SELECT items
24/// (`SELECT * FROM t`) into the underlying table's column names when deriving
25/// each CTE's schema (#5293).
26pub fn execute_ctes<F>(
27    ctes: &[vibesql_ast::CommonTableExpr],
28    database: &vibesql_storage::Database,
29    executor: F,
30) -> Result<HashMap<String, CteResult>, ExecutorError>
31where
32    F: Fn(
33        &vibesql_ast::SelectStmt,
34        &HashMap<String, CteResult>,
35    ) -> Result<Vec<vibesql_storage::Row>, ExecutorError>,
36{
37    // Use the memory-tracking version with a no-op memory check
38    execute_ctes_with_memory_check(ctes, database, executor, |_| Ok(()))
39}
40
41/// Execute all CTEs with memory tracking
42///
43/// CTEs are executed in order, allowing later CTEs to reference earlier ones.
44/// After each CTE is materialized, the memory_check callback is called with
45/// the estimated size of the CTE result to enforce memory limits.
46pub(super) fn execute_ctes_with_memory_check<F, M>(
47    ctes: &[vibesql_ast::CommonTableExpr],
48    database: &vibesql_storage::Database,
49    executor: F,
50    memory_check: M,
51) -> Result<HashMap<String, CteResult>, ExecutorError>
52where
53    F: Fn(
54        &vibesql_ast::SelectStmt,
55        &HashMap<String, CteResult>,
56    ) -> Result<Vec<vibesql_storage::Row>, ExecutorError>,
57    M: Fn(usize) -> Result<(), ExecutorError>,
58{
59    let mut cte_results = HashMap::new();
60
61    // Execute each CTE in order
62    // CTEs can reference previously defined CTEs
63    for cte in ctes {
64        // Check if this is a recursive CTE
65        // SQLite compatibility: auto-detect recursive CTEs even without RECURSIVE keyword
66        // A CTE is recursive if it references itself in a UNION/UNION ALL set operation
67        let is_recursive = cte.recursive || is_cte_self_referential(cte);
68        let rows = if is_recursive {
69            // Recursive CTE: execute base term, then iteratively execute recursive term
70            execute_recursive_cte(cte, &cte_results, database, &executor, &memory_check)?
71        } else {
72            // Non-recursive CTE: execute query directly
73            executor(&cte.query, &cte_results)?
74        };
75
76        // Track memory for this CTE result before storing
77        let estimated_size = super::helpers::estimate_result_size(&rows);
78        memory_check(estimated_size)?;
79
80        //  Determine the schema for this CTE
81        let schema = derive_cte_schema(cte, &rows, database, &cte_results)?;
82
83        // Store the CTE result wrapped in Arc for efficient sharing
84        cte_results.insert(cte.name.clone(), (schema, Arc::new(rows)));
85    }
86
87    Ok(cte_results)
88}
89
90/// Derive the schema for a CTE from its query and results
91///
92/// `database` and `prior_ctes` are used to statically expand wildcard SELECT
93/// items (`*` / `t.*`) into the column names of the underlying FROM sources
94/// (#5293). Without expansion, `WITH cte AS (SELECT * FROM t)` would
95/// materialize a single `col{i}` column, silently dropping columns.
96pub(super) fn derive_cte_schema(
97    cte: &vibesql_ast::CommonTableExpr,
98    rows: &[vibesql_storage::Row],
99    database: &vibesql_storage::Database,
100    prior_ctes: &HashMap<String, CteResult>,
101) -> Result<vibesql_catalog::TableSchema, ExecutorError> {
102    // If column names are explicitly specified, use those
103    if let Some(column_names) = &cte.columns {
104        // Get data types from first row (if available)
105        if let Some(first_row) = rows.first() {
106            if first_row.values.len() != column_names.len() {
107                return Err(ExecutorError::UnsupportedFeature(format!(
108                    "CTE column count mismatch: specified {} columns but query returned {}",
109                    column_names.len(),
110                    first_row.values.len()
111                )));
112            }
113
114            let columns = column_names
115                .iter()
116                .zip(&first_row.values)
117                .map(|(name, value)| {
118                    let data_type = infer_type_from_value(value);
119                    vibesql_catalog::ColumnSchema::new(name.clone(), data_type, true)
120                    // nullable for
121                    // simplicity
122                })
123                .collect();
124
125            Ok(cte_pseudo_schema(cte.name.clone(), columns))
126        } else {
127            // Empty result set - create schema with VARCHAR columns
128            let columns = column_names
129                .iter()
130                .map(|name| {
131                    vibesql_catalog::ColumnSchema::new(
132                        name.clone(),
133                        vibesql_types::DataType::Varchar { max_length: Some(255) },
134                        true,
135                    )
136                })
137                .collect();
138
139            Ok(cte_pseudo_schema(cte.name.clone(), columns))
140        }
141    } else {
142        // No explicit column names - infer from query SELECT list.
143        // Wildcard items are statically expanded into the column names of the
144        // underlying FROM sources (#5293). A running value offset tracks the
145        // position of each output column in the materialized rows so that
146        // type inference stays aligned after expansion (e.g. `SELECT *, expr`
147        // from a 2-column table puts `expr` at value index 2, not 1).
148        let mut columns: Vec<vibesql_catalog::ColumnSchema> = Vec::new();
149        let mut value_idx = 0usize;
150
151        for (i, item) in cte.query.select_list.iter().enumerate() {
152            // Determine the output column name(s) for this SELECT item
153            let names: Vec<String> = match item {
154                vibesql_ast::SelectItem::Wildcard { .. }
155                | vibesql_ast::SelectItem::QualifiedWildcard { .. } => {
156                    expand_wildcard_names(item, &cte.query, database, prior_ctes)
157                        // Unresolvable FROM source (e.g. a view): fall back to
158                        // the legacy positional name for this item
159                        .unwrap_or_else(|| vec![format!("col{}", i)])
160                }
161                vibesql_ast::SelectItem::Expression { expr, alias, .. } => {
162                    vec![if let Some(a) = alias {
163                        a.clone()
164                    } else {
165                        // Try to extract name from expression
166                        match expr {
167                            vibesql_ast::Expression::ColumnRef(col_id) => {
168                                col_id.column_canonical().to_string()
169                            }
170                            _ => format!("col{}", i),
171                        }
172                    }]
173                }
174            };
175
176            for name in names {
177                // Infer data type from first row if available, otherwise use default
178                let data_type = rows
179                    .first()
180                    .and_then(|first_row| first_row.values.get(value_idx))
181                    .map(infer_type_from_value)
182                    .unwrap_or(vibesql_types::DataType::Varchar { max_length: Some(255) });
183
184                columns.push(vibesql_catalog::ColumnSchema::new(name, data_type, true)); // nullable
185                value_idx += 1;
186            }
187        }
188
189        // Sanity check: if static expansion disagrees with the actual row
190        // width, the resolution was wrong (e.g. an exotic FROM source).
191        // Fall back to the legacy one-column-per-item naming rather than
192        // exposing a schema that misattributes columns.
193        if let Some(first_row) = rows.first() {
194            if columns.len() != first_row.values.len() {
195                return Ok(legacy_cte_schema(cte, rows));
196            }
197        }
198
199        Ok(cte_pseudo_schema(cte.name.clone(), columns))
200    }
201}
202
203/// Build a CTE pseudo-schema.
204///
205/// CTEs, like views and derived tables, have no implicit `rowid`: SQLite errors
206/// with `no such column: rowid` when `rowid`/`oid`/`_rowid_` is referenced
207/// against a CTE that does not explicitly declare such a column. We mark the
208/// schema with `is_view = true` so the shared rowid-resolution paths (added in
209/// #5492) reject the pseudo-column. A CTE column genuinely named `rowid` still
210/// resolves, because real columns take precedence over the pseudo-column in
211/// those paths. See issue #5516.
212fn cte_pseudo_schema(
213    name: String,
214    columns: Vec<vibesql_catalog::ColumnSchema>,
215) -> vibesql_catalog::TableSchema {
216    let mut schema = vibesql_catalog::TableSchema::new(name, columns);
217    schema.set_is_view(true);
218    schema
219}
220
221/// Legacy schema derivation: one column per SELECT item, wildcards named
222/// `col{i}`. Used only as a fallback when static wildcard expansion cannot
223/// resolve the FROM sources or disagrees with the materialized row width.
224fn legacy_cte_schema(
225    cte: &vibesql_ast::CommonTableExpr,
226    rows: &[vibesql_storage::Row],
227) -> vibesql_catalog::TableSchema {
228    let columns = cte
229        .query
230        .select_list
231        .iter()
232        .enumerate()
233        .map(|(i, item)| {
234            let data_type = rows
235                .first()
236                .and_then(|first_row| first_row.values.get(i))
237                .map(infer_type_from_value)
238                .unwrap_or(vibesql_types::DataType::Varchar { max_length: Some(255) });
239
240            let col_name = match item {
241                vibesql_ast::SelectItem::Wildcard { .. }
242                | vibesql_ast::SelectItem::QualifiedWildcard { .. } => format!("col{}", i),
243                vibesql_ast::SelectItem::Expression { expr, alias, .. } => {
244                    if let Some(a) = alias {
245                        a.clone()
246                    } else {
247                        match expr {
248                            vibesql_ast::Expression::ColumnRef(col_id) => {
249                                col_id.column_canonical().to_string()
250                            }
251                            _ => format!("col{}", i),
252                        }
253                    }
254                }
255            };
256
257            vibesql_catalog::ColumnSchema::new(col_name, data_type, true) // nullable
258        })
259        .collect();
260
261    cte_pseudo_schema(cte.name.clone(), columns)
262}
263
264/// A column of a FROM-clause source resolved for wildcard expansion.
265///
266/// `hidden_for_star` marks right-side NATURAL/USING join columns that are
267/// deduplicated out of plain-`*` expansion. Qualified wildcards (`t.*`) keep
268/// ALL of the source's columns, including hidden ones, matching SQLite
269/// (`SELECT b.* FROM a NATURAL JOIN b` returns the join column too).
270struct WildcardColumn {
271    name: String,
272    hidden_for_star: bool,
273}
274
275/// A FROM-clause source resolved for wildcard expansion: the effective
276/// qualifier (alias if present, else table name) and its column names.
277struct WildcardSource {
278    qualifier: String,
279    columns: Vec<WildcardColumn>,
280}
281
282/// Wrap plain column names as star-visible wildcard columns.
283fn visible_columns(names: Vec<String>) -> Vec<WildcardColumn> {
284    names.into_iter().map(|name| WildcardColumn { name, hidden_for_star: false }).collect()
285}
286
287/// Resolve a catalog view's output column names *statically*, without executing
288/// its body.
289///
290/// Prefers the view's stored column list (populated at CREATE VIEW time). When
291/// that is absent, derives the names from the view's defining query's SELECT
292/// list (recursively expanding wildcards over its own sources). Returns `None`
293/// when `name` is not a view or its columns cannot be determined statically.
294fn view_column_names(name: &str, database: &vibesql_storage::Database) -> Option<Vec<String>> {
295    let view = database.catalog.get_view(name)?;
296    if let Some(cols) = &view.columns {
297        return Some(cols.clone());
298    }
299    // Fall back to deriving from the view's defining query. No prior CTEs are in
300    // scope for a stored view definition.
301    collect_select_list_columns(&view.query, database, &HashMap::new())
302}
303
304/// Expand a wildcard SELECT item (`*` or `qualifier.*`) into column names
305/// using the statement's FROM clause.
306///
307/// Returns `None` if any FROM source cannot be resolved statically (e.g. a
308/// view); callers fall back to legacy `col{i}` naming.
309fn expand_wildcard_names(
310    item: &vibesql_ast::SelectItem,
311    stmt: &vibesql_ast::SelectStmt,
312    database: &vibesql_storage::Database,
313    prior_ctes: &HashMap<String, CteResult>,
314) -> Option<Vec<String>> {
315    match item {
316        vibesql_ast::SelectItem::Wildcard { alias } => {
317            // SQL:1999 E051-07 derived column list: SELECT * AS (a, b, ...)
318            if let Some(alias_names) = alias {
319                return Some(alias_names.clone());
320            }
321            let sources = collect_from_sources(stmt.from.as_ref()?, database, prior_ctes)?;
322            Some(
323                sources
324                    .into_iter()
325                    .flat_map(|s| s.columns)
326                    .filter(|c| !c.hidden_for_star)
327                    .map(|c| c.name)
328                    .collect(),
329            )
330        }
331        vibesql_ast::SelectItem::QualifiedWildcard { qualifier, alias } => {
332            if let Some(alias_names) = alias {
333                return Some(alias_names.clone());
334            }
335            let sources = collect_from_sources(stmt.from.as_ref()?, database, prior_ctes)?;
336            sources
337                .into_iter()
338                .find(|s| s.qualifier.eq_ignore_ascii_case(qualifier))
339                // Qualified wildcards keep ALL of the source's columns,
340                // including NATURAL/USING join columns hidden from plain `*`
341                .map(|s| s.columns.into_iter().map(|c| c.name).collect())
342        }
343        vibesql_ast::SelectItem::Expression { .. } => None,
344    }
345}
346
347/// Resolve the sources of a FROM clause to their column names for wildcard
348/// expansion. Mirrors the traversal in
349/// `evaluator::combined::subqueries::schema_utils::count_columns_in_from_clause`
350/// but collects names instead of counts.
351///
352/// Returns `None` when a source cannot be resolved statically; callers fall
353/// back to legacy naming rather than erroring.
354fn collect_from_sources(
355    from: &vibesql_ast::FromClause,
356    database: &vibesql_storage::Database,
357    prior_ctes: &HashMap<String, CteResult>,
358) -> Option<Vec<WildcardSource>> {
359    match from {
360        vibesql_ast::FromClause::Table { name, alias, column_aliases, .. } => {
361            // Check prior CTEs first (case-insensitive, matching the
362            // resolution convention used elsewhere), then database tables
363            let base_columns: Vec<String> = if let Some((schema, _)) =
364                prior_ctes.get(name).or_else(|| {
365                    prior_ctes.iter().find(|(k, _)| k.eq_ignore_ascii_case(name)).map(|(_, v)| v)
366                }) {
367                schema.columns.iter().map(|c| c.name.clone()).collect()
368            } else if let Some(table) = database.get_table(name) {
369                table.schema.columns.iter().map(|c| c.name.clone()).collect()
370            } else if let Some(cols) = view_column_names(name, database) {
371                // A catalog view whose output columns are known statically.
372                // Resolving views from their stored column list (rather than
373                // executing their bodies) is essential to keep deeply nested
374                // views cheap: it lets CREATE VIEW derive columns without the
375                // exponential re-materialization of doubling view nests
376                // (#5394, view3.test).
377                cols
378            } else {
379                // Unknown source we cannot resolve statically.
380                return None;
381            };
382
383            // SQL:1999 E051-09: FROM t AS a(x, y) renames the columns
384            let columns = match column_aliases {
385                Some(aliases) if aliases.len() == base_columns.len() => aliases.clone(),
386                Some(_) => return None, // mismatched rename list - bail out
387                None => base_columns,
388            };
389
390            let qualifier = alias.clone().unwrap_or_else(|| name.clone());
391            Some(vec![WildcardSource { qualifier, columns: visible_columns(columns) }])
392        }
393        vibesql_ast::FromClause::Join { left, right, natural, using_columns, alias, .. } => {
394            // Non-goal: aliased parenthesized NATURAL/USING joins
395            // (`(a JOIN b USING(k)) AS j`) hoist the USING columns to the
396            // front under SQLite semantics (#4916). Static expansion does not
397            // model that reordering, so fall back to legacy naming.
398            if alias.is_some() && (*natural || using_columns.is_some()) {
399                return None;
400            }
401
402            let mut sources = collect_from_sources(left, database, prior_ctes)?;
403            let mut right_sources = collect_from_sources(right, database, prior_ctes)?;
404
405            // NATURAL/USING joins deduplicate the shared columns out of
406            // plain-`*` expansion: ALL left columns stay in declaration order
407            // (join columns are NOT hoisted to the front), then the right
408            // columns minus the shared ones. This mirrors the runtime
409            // expansion in `select/projection.rs` (issue #4916 ordering).
410            if *natural || using_columns.is_some() {
411                let shared: Vec<String> = if let Some(using) = using_columns {
412                    using.clone()
413                } else {
414                    // NATURAL: case-insensitive intersection of the left
415                    // operand's star-visible names with the right operand's
416                    // star-visible names. Using star-visible (already
417                    // deduplicated) names makes chained NATURAL joins compute
418                    // each join's shared set against the accumulated output.
419                    let left_visible: Vec<&str> = sources
420                        .iter()
421                        .flat_map(|s| s.columns.iter())
422                        .filter(|c| !c.hidden_for_star)
423                        .map(|c| c.name.as_str())
424                        .collect();
425                    right_sources
426                        .iter()
427                        .flat_map(|s| s.columns.iter())
428                        .filter(|c| !c.hidden_for_star)
429                        .filter(|c| left_visible.iter().any(|l| l.eq_ignore_ascii_case(&c.name)))
430                        .map(|c| c.name.clone())
431                        .collect()
432                };
433
434                // Hide the shared columns on the right side only; they remain
435                // resolvable through qualified wildcards (`b.*`).
436                for source in &mut right_sources {
437                    for col in &mut source.columns {
438                        if shared.iter().any(|s| s.eq_ignore_ascii_case(&col.name)) {
439                            col.hidden_for_star = true;
440                        }
441                    }
442                }
443            }
444
445            sources.extend(right_sources);
446            Some(sources)
447        }
448        vibesql_ast::FromClause::Subquery { query, alias, column_aliases } => {
449            let columns = if let Some(aliases) = column_aliases {
450                aliases.clone()
451            } else {
452                collect_select_list_columns(query, database, prior_ctes)?
453            };
454            Some(vec![WildcardSource {
455                qualifier: alias.clone(),
456                columns: visible_columns(columns),
457            }])
458        }
459        vibesql_ast::FromClause::Values { rows, alias, column_aliases } => {
460            let columns = if let Some(aliases) = column_aliases {
461                aliases.clone()
462            } else {
463                let first_row = rows.first()?;
464                (0..first_row.len()).map(|i| format!("col{}", i)).collect()
465            };
466            Some(vec![WildcardSource {
467                qualifier: alias.clone(),
468                columns: visible_columns(columns),
469            }])
470        }
471    }
472}
473
474/// Statically compute a SELECT statement's output column names, expanding
475/// wildcards (resolving tables, CTEs, and views from catalog metadata) without
476/// executing the query.
477///
478/// For compound queries (UNION/INTERSECT/EXCEPT) the column names come from the
479/// leftmost SELECT, matching SQL/SQLite semantics.
480///
481/// Used by CREATE VIEW to derive a view's columns cheaply, avoiding the
482/// exponential re-materialization of deeply nested views (#5394, view3.test).
483/// Returns `None` when names cannot be determined statically; callers then fall
484/// back to executing the body.
485pub(crate) fn try_static_select_columns(
486    stmt: &vibesql_ast::SelectStmt,
487    database: &vibesql_storage::Database,
488) -> Option<Vec<String>> {
489    collect_select_list_columns(stmt, database, &HashMap::new())
490}
491
492/// Compute the output column names of a SELECT statement, expanding any
493/// wildcard items. Used to resolve subqueries appearing in a FROM clause.
494///
495/// Returns `None` if names cannot be determined statically.
496fn collect_select_list_columns(
497    stmt: &vibesql_ast::SelectStmt,
498    database: &vibesql_storage::Database,
499    prior_ctes: &HashMap<String, CteResult>,
500) -> Option<Vec<String>> {
501    // VALUES statement: names come from the first row's width
502    if let Some(values_rows) = &stmt.values {
503        let first_row = values_rows.first()?;
504        return Some((0..first_row.len()).map(|i| format!("col{}", i)).collect());
505    }
506
507    let mut names = Vec::new();
508    for (i, item) in stmt.select_list.iter().enumerate() {
509        match item {
510            vibesql_ast::SelectItem::Wildcard { .. }
511            | vibesql_ast::SelectItem::QualifiedWildcard { .. } => {
512                names.extend(expand_wildcard_names(item, stmt, database, prior_ctes)?);
513            }
514            vibesql_ast::SelectItem::Expression { expr, alias, .. } => {
515                names.push(if let Some(a) = alias {
516                    a.clone()
517                } else {
518                    match expr {
519                        vibesql_ast::Expression::ColumnRef(col_id) => {
520                            col_id.column_canonical().to_string()
521                        }
522                        _ => format!("col{}", i),
523                    }
524                });
525            }
526        }
527    }
528    Some(names)
529}
530
531/// Execute a recursive CTE using iterative evaluation
532///
533/// Recursive CTEs in SQL:1999/SQLite are defined with UNION or UNION ALL:
534/// ```sql
535/// WITH RECURSIVE cte AS (
536///   base_query          -- Executed once to get initial rows
537///   UNION [ALL]
538///   recursive_query     -- References 'cte', executed iteratively
539/// )
540/// ```
541///
542/// Algorithm:
543/// 1. Split query into base and recursive terms (before/after UNION [ALL])
544/// 2. Execute base term to get initial working table
545/// 3. Repeat until no new rows or max depth reached:
546///    - Make working table available as CTE
547///    - Execute recursive term
548///    - Add new rows to result (with deduplication for UNION)
549///    - Update working table to new rows
550fn execute_recursive_cte<F, M>(
551    cte: &vibesql_ast::CommonTableExpr,
552    cte_results: &HashMap<String, CteResult>,
553    database: &vibesql_storage::Database,
554    executor: &F,
555    memory_check: &M,
556) -> Result<Vec<vibesql_storage::Row>, ExecutorError>
557where
558    F: Fn(
559        &vibesql_ast::SelectStmt,
560        &HashMap<String, CteResult>,
561    ) -> Result<Vec<vibesql_storage::Row>, ExecutorError>,
562    M: Fn(usize) -> Result<(), ExecutorError>,
563{
564    use crate::limits::MAX_RECURSIVE_CTE_ITERATIONS;
565
566    // Validate that recursive CTE uses UNION ALL
567    let set_op = cte.query.set_operation.as_ref().ok_or_else(|| {
568        ExecutorError::UnsupportedFeature(format!(
569            "Recursive CTE '{}' must use UNION ALL",
570            cte.name
571        ))
572    })?;
573
574    if set_op.op != vibesql_ast::SetOperator::Union {
575        return Err(ExecutorError::UnsupportedFeature(format!(
576            "Recursive CTE '{}' must use UNION or UNION ALL (not INTERSECT or EXCEPT)",
577            cte.name
578        )));
579    }
580
581    // Extract base and recursive terms
582    // Base term: the main SELECT (before UNION [ALL])
583    // Recursive term: the right side of UNION [ALL]
584
585    // Create base-only query without the UNION ALL set operation
586    // This prevents the base term from trying to reference the CTE before it exists
587    let base_query = vibesql_ast::SelectStmt {
588        with_clause: cte.query.with_clause.clone(),
589        distinct: cte.query.distinct,
590        select_list: cte.query.select_list.clone(),
591        into_table: cte.query.into_table.clone(),
592        into_variables: cte.query.into_variables.clone(),
593        from: cte.query.from.clone(),
594        where_clause: cte.query.where_clause.clone(),
595        group_by: cte.query.group_by.clone(),
596        having: cte.query.having.clone(),
597        window_definitions: cte.query.window_definitions.clone(),
598        order_by: cte.query.order_by.clone(),
599        limit: cte.query.limit.clone(),
600        offset: cte.query.offset.clone(),
601        set_operation: None, // Remove UNION ALL for base term execution
602        values: cte.query.values.clone(),
603    };
604    let recursive_query = &set_op.right;
605
606    // SQLite compatibility: window functions are not allowed in the recursive
607    // part of a recursive CTE (window1.test 15.0). SQLite reports the exact
608    // error "cannot use window functions in recursive queries".
609    if crate::select::window::has_window_functions(&recursive_query.select_list) {
610        return Err(ExecutorError::SqliteCompatError(
611            "cannot use window functions in recursive queries".to_string(),
612        ));
613    }
614
615    // Try static validation first (works for explicit column lists and VALUES)
616    // This provides better SQLite compatibility by catching errors at prepare time
617    // rather than waiting until runtime
618    // Note: For VALUES statements, column count comes from the VALUES rows, not select_list
619    if let (Some(base_count), Some(recursive_count)) =
620        (count_stmt_columns(&base_query), count_stmt_columns(recursive_query))
621    {
622        if base_count != recursive_count {
623            return Err(ExecutorError::UnsupportedFeature(
624                "SELECTs to the left and right of UNION ALL do not have the same number of result columns".to_string()
625            ));
626        }
627    }
628    // Fall back to runtime validation for wildcards (existing code below at line 279-289)
629
630    // Step 1: Execute base term to get initial rows
631    let mut all_rows = executor(&base_query, cte_results)?;
632    let mut working_table = all_rows.clone();
633
634    // Derive schema from base term result
635    // Wildcards in the base term are expanded against database tables and
636    // prior CTEs (#5293)
637    let schema = derive_cte_schema(cte, &all_rows, database, cte_results)?;
638
639    // Track seen rows for UNION (deduplication)
640    // For UNION ALL, we skip tracking to preserve all rows
641    let mut seen_rows: Option<HashSet<vibesql_storage::RowValues>> = if !set_op.all {
642        let mut seen = HashSet::with_capacity(all_rows.len());
643        for row in &all_rows {
644            seen.insert(row.values.clone());
645        }
646        Some(seen)
647    } else {
648        None
649    };
650
651    // Step 2: Iterative evaluation
652    let mut depth = 0;
653    while !working_table.is_empty() && depth < MAX_RECURSIVE_CTE_ITERATIONS {
654        depth += 1;
655
656        // Make working table available as this CTE for recursive reference
657        let mut recursive_cte_results = cte_results.clone();
658        recursive_cte_results
659            .insert(cte.name.clone(), (schema.clone(), Arc::new(working_table.clone())));
660
661        // Execute recursive term with working table as CTE
662        let new_rows = executor(recursive_query, &recursive_cte_results)?;
663
664        // If no new rows, we're done
665        if new_rows.is_empty() {
666            break;
667        }
668
669        // Validate that recursive term returns same number of columns as base term
670        // This check is done on first iteration to catch schema mismatches early
671        if depth == 1 && !new_rows.is_empty() && !all_rows.is_empty() {
672            let base_col_count = all_rows[0].values.len();
673            let recursive_col_count = new_rows[0].values.len();
674            if base_col_count != recursive_col_count {
675                return Err(ExecutorError::UnsupportedFeature(
676                    "SELECTs to the left and right of UNION ALL do not have the same number of result columns".to_string()
677                ));
678            }
679        }
680
681        // Check memory before adding new rows
682        let estimated_size = super::helpers::estimate_result_size(&new_rows);
683        memory_check(estimated_size)?;
684
685        // Filter out duplicates for UNION (keep all for UNION ALL)
686        let rows_to_add: Vec<vibesql_storage::Row> = if let Some(ref mut seen) = seen_rows {
687            // UNION: only add rows we haven't seen before
688            new_rows.into_iter().filter(|row| seen.insert(row.values.clone())).collect()
689        } else {
690            // UNION ALL: keep all rows
691            new_rows
692        };
693
694        // If no new unique rows (for UNION), we're done
695        if rows_to_add.is_empty() {
696            break;
697        }
698
699        // Add new rows to result
700        all_rows.extend(rows_to_add.clone());
701
702        // Update working table to be the new rows for next iteration
703        working_table = rows_to_add;
704    }
705
706    // Check if we hit max recursion depth
707    if depth >= MAX_RECURSIVE_CTE_ITERATIONS {
708        return Err(ExecutorError::UnsupportedFeature(format!(
709            "Recursive CTE '{}' exceeded maximum iteration limit of {}",
710            cte.name, MAX_RECURSIVE_CTE_ITERATIONS
711        )));
712    }
713
714    Ok(all_rows)
715}
716
717/// Count columns if select list has only explicit expressions (no wildcards)
718///
719/// Returns Some(count) if all select items are explicit expressions.
720/// Returns None if any wildcards are present (requires schema info to count).
721fn count_explicit_columns(select_list: &[vibesql_ast::SelectItem]) -> Option<usize> {
722    let mut count = 0;
723    for item in select_list {
724        match item {
725            vibesql_ast::SelectItem::Expression { .. } => count += 1,
726            // Can't count wildcards statically - need schema info
727            vibesql_ast::SelectItem::Wildcard { .. }
728            | vibesql_ast::SelectItem::QualifiedWildcard { .. } => {
729                return None;
730            }
731        }
732    }
733    Some(count)
734}
735
736/// Count columns in a SELECT statement, considering both select_list and VALUES.
737/// For VALUES statements, the column count comes from the first row of values.
738/// For SELECT statements, the column count comes from the select_list.
739/// Returns None if any wildcards are present (requires schema info to count).
740fn count_stmt_columns(stmt: &vibesql_ast::SelectStmt) -> Option<usize> {
741    // If this is a VALUES statement, count columns from the first VALUES row
742    if let Some(values_rows) = &stmt.values {
743        return values_rows.first().map(|row| row.len());
744    }
745
746    // Otherwise, count columns from the select_list
747    count_explicit_columns(&stmt.select_list)
748}
749
750/// Check if a CTE is self-referential (references itself in UNION/UNION ALL)
751///
752/// SQLite allows recursive CTEs without the RECURSIVE keyword if the CTE
753/// references itself in a set operation. This function detects such cases
754/// by checking if the right side of a UNION/UNION ALL references the CTE name.
755fn is_cte_self_referential(cte: &vibesql_ast::CommonTableExpr) -> bool {
756    // Check if the CTE has a UNION/UNION ALL set operation
757    let set_op = match &cte.query.set_operation {
758        Some(op) if op.op == vibesql_ast::SetOperator::Union => op,
759        _ => return false,
760    };
761
762    // Check if the recursive term references this CTE
763    stmt_references_table(&set_op.right, &cte.name)
764}
765
766/// Check if a SELECT statement references a table name
767fn stmt_references_table(stmt: &vibesql_ast::SelectStmt, table_name: &str) -> bool {
768    // Check FROM clause
769    if let Some(from) = &stmt.from {
770        if from_clause_references_table(from, table_name) {
771            return true;
772        }
773    }
774
775    // Check WHERE clause for subqueries
776    if let Some(where_clause) = &stmt.where_clause {
777        if expr_references_table(where_clause, table_name) {
778            return true;
779        }
780    }
781
782    // Check SELECT list for subqueries
783    for item in &stmt.select_list {
784        if let vibesql_ast::SelectItem::Expression { expr, .. } = item {
785            if expr_references_table(expr, table_name) {
786                return true;
787            }
788        }
789    }
790
791    false
792}
793
794/// Check if a FROM clause references a table name
795fn from_clause_references_table(from: &vibesql_ast::FromClause, table_name: &str) -> bool {
796    match from {
797        vibesql_ast::FromClause::Table { name, .. } => name.eq_ignore_ascii_case(table_name),
798        vibesql_ast::FromClause::Subquery { query, .. } => stmt_references_table(query, table_name),
799        vibesql_ast::FromClause::Join { left, right, condition, .. } => {
800            from_clause_references_table(left, table_name)
801                || from_clause_references_table(right, table_name)
802                || condition.as_ref().map_or(false, |c| expr_references_table(c, table_name))
803        }
804        vibesql_ast::FromClause::Values { .. } => false,
805    }
806}
807
808/// Check if an expression references a table name (in subqueries)
809fn expr_references_table(expr: &vibesql_ast::Expression, table_name: &str) -> bool {
810    match expr {
811        vibesql_ast::Expression::ScalarSubquery(subquery) => {
812            stmt_references_table(subquery, table_name)
813        }
814        vibesql_ast::Expression::In { subquery, .. } => stmt_references_table(subquery, table_name),
815        vibesql_ast::Expression::Exists { subquery, .. } => {
816            stmt_references_table(subquery, table_name)
817        }
818        vibesql_ast::Expression::BinaryOp { left, right, .. } => {
819            expr_references_table(left, table_name) || expr_references_table(right, table_name)
820        }
821        vibesql_ast::Expression::UnaryOp { expr, .. } => expr_references_table(expr, table_name),
822        vibesql_ast::Expression::Function { args, .. } => {
823            args.iter().any(|arg| expr_references_table(arg, table_name))
824        }
825        vibesql_ast::Expression::AggregateFunction { args, filter, .. } => {
826            args.iter().any(|arg| expr_references_table(arg, table_name))
827                || filter.as_ref().map_or(false, |f| expr_references_table(f, table_name))
828        }
829        vibesql_ast::Expression::Case { operand, when_clauses, else_result, .. } => {
830            operand.as_ref().map_or(false, |o| expr_references_table(o, table_name))
831                || when_clauses.iter().any(|when| {
832                    when.conditions.iter().any(|c| expr_references_table(c, table_name))
833                        || expr_references_table(&when.result, table_name)
834                })
835                || else_result.as_ref().map_or(false, |e| expr_references_table(e, table_name))
836        }
837        vibesql_ast::Expression::Between { expr, low, high, .. } => {
838            expr_references_table(expr, table_name)
839                || expr_references_table(low, table_name)
840                || expr_references_table(high, table_name)
841        }
842        vibesql_ast::Expression::InList { expr, values, .. } => {
843            expr_references_table(expr, table_name)
844                || values.iter().any(|e| expr_references_table(e, table_name))
845        }
846        vibesql_ast::Expression::Cast { expr, .. }
847        | vibesql_ast::Expression::Collate { expr, .. } => expr_references_table(expr, table_name),
848        vibesql_ast::Expression::Conjunction(exprs)
849        | vibesql_ast::Expression::Disjunction(exprs) => {
850            exprs.iter().any(|e| expr_references_table(e, table_name))
851        }
852        vibesql_ast::Expression::QuantifiedComparison { expr, subquery, .. } => {
853            expr_references_table(expr, table_name) || stmt_references_table(subquery, table_name)
854        }
855        _ => false,
856    }
857}
858
859/// Infer data type from a SQL value
860pub(super) fn infer_type_from_value(value: &vibesql_types::SqlValue) -> vibesql_types::DataType {
861    match value {
862        vibesql_types::SqlValue::Null => vibesql_types::DataType::Varchar { max_length: Some(255) }, /* default */
863        vibesql_types::SqlValue::Integer(_) => vibesql_types::DataType::Integer,
864        vibesql_types::SqlValue::Varchar(_) => {
865            vibesql_types::DataType::Varchar { max_length: Some(255) }
866        }
867        vibesql_types::SqlValue::Character(_) => vibesql_types::DataType::Character { length: 1 },
868        vibesql_types::SqlValue::Boolean(_) => vibesql_types::DataType::Boolean,
869        vibesql_types::SqlValue::Float(_) => vibesql_types::DataType::Float { precision: 53 },
870        vibesql_types::SqlValue::Double(_) => vibesql_types::DataType::DoublePrecision,
871        vibesql_types::SqlValue::Numeric(_) => {
872            vibesql_types::DataType::Numeric { precision: 10, scale: 2 }
873        }
874        vibesql_types::SqlValue::Real(_) => vibesql_types::DataType::Real,
875        vibesql_types::SqlValue::Smallint(_) => vibesql_types::DataType::Smallint,
876        vibesql_types::SqlValue::Bigint(_) => vibesql_types::DataType::Bigint,
877        vibesql_types::SqlValue::Unsigned(_) => vibesql_types::DataType::Unsigned,
878        vibesql_types::SqlValue::Date(_) => vibesql_types::DataType::Date,
879        vibesql_types::SqlValue::Time(_) => vibesql_types::DataType::Time { with_timezone: false },
880        vibesql_types::SqlValue::Timestamp(_) => {
881            vibesql_types::DataType::Timestamp { with_timezone: false }
882        }
883        vibesql_types::SqlValue::Interval(_) => {
884            // For now, return a simple INTERVAL type (can be enhanced to detect field types)
885            vibesql_types::DataType::Interval {
886                start_field: vibesql_types::IntervalField::Day,
887                end_field: None,
888            }
889        }
890        vibesql_types::SqlValue::Vector(v) => {
891            vibesql_types::DataType::Vector { dimensions: v.len() as u32 }
892        }
893        vibesql_types::SqlValue::Blob(_) => vibesql_types::DataType::BinaryLargeObject,
894    }
895}