Skip to main content

radixdb_executor/cte/
mod.rs

1// Copyright 2026 RadixDB Contributors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Common Table Expression (CTE) Execution
16//!
17//! This module implements WITH clause execution for SQL queries.
18//!
19//! Supports:
20//! - Basic CTEs: `WITH x AS (SELECT ...) SELECT * FROM x`
21//! - Multiple CTEs: `WITH a AS (...), b AS (...) SELECT ...`
22//! - CTE with column aliases: `WITH x(col1, col2) AS (SELECT ...) SELECT ...`
23//! - CTEs referencing other CTEs: `WITH a AS (...), b AS (SELECT * FROM a) ...`
24//!
25//! Optimizations:
26//! - CTE Inlining: Single-use, non-recursive CTEs are converted to subqueries
27//!   to preserve index access and benefit from LIMIT pushdown
28//!
29//! Note: Recursive CTEs are parsed but not yet executed.
30
31use ahash::AHashSet;
32use std::sync::{Arc, OnceLock};
33
34use radixdb_core::{CompactArc, CompactVec, StringMap};
35use radixdb_core::{DataType, Error, Result, Row, RowVec, Value};
36use radixdb_functions::FunctionRegistry;
37use radixdb_sql::ast::*;
38use radixdb_sql::token::{Position, Token, TokenType};
39use radixdb_storage::traits::QueryResult;
40
41use super::aggregation::{AggregationExecutorExt, AggregationHost};
42use super::context::ExecutionContext;
43use super::expression::{compile_expression_with_context, ExpressionEval};
44use super::pipeline::paging::evaluate_page_expression;
45use super::pipeline::set::merge_set_type;
46use super::query_classification::{get_classification, QueryClassification};
47use super::subquery::{SubqueryExecutorExt, SubqueryHost};
48use super::utils::build_column_index_map;
49use super::utils::RetainedRowsBudget;
50use super::window::{WindowExecutorExt, WindowHost};
51
52/// Type alias for CTE data: (columns, rows) with Arc for zero-copy sharing
53/// Uses `Vec<(i64, Row)>` for rows - same structure as `RowVec` but Arc-shareable
54pub type CteData = (
55    CompactArc<Vec<String>>,
56    CompactArc<Vec<(i64, Row)>>,
57    Arc<OnceLock<CompactArc<Vec<Row>>>>,
58);
59
60/// Type alias for CTE data map
61/// Uses `CompactArc<Vec<String>>` for columns and `CompactArc<Vec<(i64, Row)>>` for rows
62/// to enable zero-copy sharing of CTE results with joins
63pub type CteDataMap = StringMap<CteData>;
64
65/// Registry for CTE results during query execution
66///
67/// Uses Arc with COW (copy-on-write) semantics to avoid cloning:
68/// - During building: `Arc::make_mut` gives mutable access without cloning (single owner)
69/// - During sharing: `data()` returns cheap Arc clone (O(1), no data copy)
70#[derive(Clone)]
71pub struct CteRegistry {
72    /// Materialized CTE results (name -> (columns, rows))
73    /// Arc provides cheap sharing; make_mut provides COW for modifications
74    data: Arc<CteDataMap>,
75}
76
77impl Default for CteRegistry {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83impl CteRegistry {
84    /// Create a new CTE registry
85    pub fn new() -> Self {
86        Self {
87            data: Arc::new(StringMap::new()),
88        }
89    }
90
91    /// Store a materialized CTE result
92    ///
93    /// Uses Arc::make_mut for COW semantics:
94    /// - If we're the only owner, mutates in place (no clone)
95    /// - If shared, clones first then mutates (preserves other references)
96    ///
97    /// Both columns and rows are wrapped in Arc to enable zero-copy sharing.
98    /// Accepts RowVec and converts to CompactArc<Vec<(i64, Row)>> for sharing.
99    pub fn store(&mut self, name: &str, columns: Vec<String>, rows: RowVec) {
100        let name_lower = name.to_lowercase();
101        // Convert RowVec to Vec<(i64, Row)> for Arc sharing
102        let rows_vec: Vec<(i64, Row)> = rows.into_iter().collect();
103        Arc::make_mut(&mut self.data).insert(
104            name_lower,
105            (
106                CompactArc::new(columns),
107                CompactArc::new(rows_vec),
108                Arc::new(OnceLock::new()),
109            ),
110        );
111    }
112
113    /// Store a materialized CTE result with pre-wrapped Arcs
114    ///
115    /// Use this when you already have Arc-wrapped data to avoid cloning.
116    /// This enables zero-copy sharing of CTE results between queries.
117    pub fn store_arc(
118        &mut self,
119        name: &str,
120        columns: CompactArc<Vec<String>>,
121        rows: CompactArc<Vec<(i64, Row)>>,
122        materialized_rows: Arc<OnceLock<CompactArc<Vec<Row>>>>,
123    ) {
124        let name_lower = name.to_lowercase();
125        Arc::make_mut(&mut self.data).insert(name_lower, (columns, rows, materialized_rows));
126    }
127
128    /// Look up a materialized CTE by case-insensitive name.
129    pub fn get(&self, name: &str) -> Option<&CteData> {
130        self.data.get(&name.to_lowercase())
131    }
132
133    /// Get a shared Arc reference to the internal data map for context transfer
134    ///
135    /// This is always O(1) - just an Arc reference count increment.
136    /// No data cloning ever happens here.
137    pub fn data(&self) -> Arc<CteDataMap> {
138        self.data.clone()
139    }
140
141    /// Iterate over all stored CTEs (for copying to temp registries)
142    pub fn iter(&self) -> impl Iterator<Item = (&String, &CteData)> {
143        self.data.iter()
144    }
145}
146
147/// Narrow composition contract for CTE execution. CTE materialization and
148/// inlining stay executor-owned; the host only provides the recursive SELECT
149/// entrypoint and immutable function registry.
150pub trait CteHost: AggregationHost + SubqueryHost + WindowHost {
151    fn cte_function_registry(&self) -> &FunctionRegistry;
152    fn cte_execute_select(
153        &self,
154        statement: &SelectStatement,
155        context: &ExecutionContext,
156    ) -> Result<Box<dyn QueryResult>>;
157}
158
159/// Single owner for CTE registries, materialization, recursive fixpoints and
160/// safe inlining decisions.
161pub struct CteExecutor<'a, H: CteHost + ?Sized> {
162    host: &'a H,
163}
164
165impl<'a, H: CteHost + ?Sized> CteExecutor<'a, H> {
166    fn new(host: &'a H) -> Self {
167        Self { host }
168    }
169}
170
171/// Internal call surface between CTE expansion and the SELECT owner.
172pub trait CteExecutorExt: CteHost {
173    fn execute_select_with_ctes(
174        &self,
175        statement: &SelectStatement,
176        context: &ExecutionContext,
177    ) -> Result<Box<dyn QueryResult>> {
178        CteExecutor::new(self).execute_select_with_ctes(statement, context)
179    }
180
181    fn execute_query_on_cte_result(
182        &self,
183        statement: &SelectStatement,
184        context: &ExecutionContext,
185        columns: Vec<String>,
186        rows: RowVec,
187    ) -> Result<(Vec<String>, RowVec)> {
188        CteExecutor::new(self).execute_query_on_cte_result(statement, context, columns, rows)
189    }
190
191    fn execute_query_on_cte_result_inner(
192        &self,
193        statement: &SelectStatement,
194        context: &ExecutionContext,
195        columns: Vec<String>,
196        rows: RowVec,
197        skip_order_limit: bool,
198    ) -> Result<(Vec<String>, RowVec, bool)> {
199        CteExecutor::new(self).execute_query_on_cte_result_inner(
200            statement,
201            context,
202            columns,
203            rows,
204            skip_order_limit,
205        )
206    }
207
208    fn has_cte(&self, statement: &SelectStatement) -> bool {
209        CteExecutor::new(self).has_cte(statement)
210    }
211
212    fn try_inline_ctes(
213        &self,
214        statement: &SelectStatement,
215        with_clause: &WithClause,
216    ) -> Option<SelectStatement> {
217        CteExecutor::new(self).try_inline_ctes(statement, with_clause)
218    }
219}
220
221impl<T: CteHost + ?Sized> CteExecutorExt for T {}
222
223fn materialize_result(mut result: Box<dyn QueryResult>) -> Result<RowVec> {
224    let mut rows = result
225        .estimated_count()
226        .map_or_else(RowVec::new, RowVec::with_capacity);
227    let mut row_id = 0i64;
228    while result.next() {
229        rows.push((row_id, result.take_row()));
230        row_id += 1;
231    }
232    if let Some(error) = result.last_error() {
233        return Err(error);
234    }
235    Ok(rows)
236}
237
238impl<H: CteHost + ?Sized> CteExecutor<'_, H> {
239    /// Execute a SELECT statement with WITH clause (CTEs)
240    pub(crate) fn execute_select_with_ctes(
241        &self,
242        stmt: &SelectStatement,
243        ctx: &ExecutionContext,
244    ) -> Result<Box<dyn QueryResult>> {
245        // Get the WITH clause
246        let with_clause = match &stmt.with {
247            Some(with) => with,
248            None => return self.host.cte_execute_select(stmt, ctx),
249        };
250
251        // CTE INLINING OPTIMIZATION:
252        // For single-use, non-recursive CTEs, convert to subqueries to:
253        // 1. Preserve index access (CTEs lose indexes when materialized)
254        // 2. Enable LIMIT pushdown through subqueries
255        // This is similar to PostgreSQL 12+'s CTE inlining behavior
256        if let Some(inlined_stmt) = self.try_inline_ctes(stmt, with_clause) {
257            // Execute the rewritten query (without WITH clause or with fewer CTEs)
258            return self.host.cte_execute_select(&inlined_stmt, ctx);
259        }
260
261        // Create CTE registry
262        let mut cte_registry = CteRegistry::new();
263
264        // Execute each CTE in order
265        for cte in &with_clause.ctes {
266            // Execute the CTE query (handles recursive CTEs)
267            let (columns, rows) = if cte.is_recursive {
268                // Pass column aliases to recursive CTE execution so they're available during iteration
269                let aliases = if cte.column_names.is_empty() {
270                    None
271                } else {
272                    Some(cte.column_names.as_slice())
273                };
274                self.execute_recursive_cte_with_columns(
275                    &cte.name.value,
276                    &cte.query,
277                    ctx,
278                    &mut cte_registry,
279                    aliases,
280                )?
281            } else {
282                self.execute_cte_query(&cte.query, ctx, &mut cte_registry)?
283            };
284
285            // Apply column aliases if specified
286            let columns = if !cte.column_names.is_empty() {
287                cte.column_names
288                    .iter()
289                    .enumerate()
290                    .map(|(i, alias)| {
291                        if i < columns.len() {
292                            alias.value.to_string()
293                        } else {
294                            columns
295                                .get(i)
296                                .cloned()
297                                .unwrap_or_else(|| format!("col{}", i))
298                        }
299                    })
300                    .collect()
301            } else {
302                columns
303            };
304
305            // Store the materialized result
306            cte_registry.store(&cte.name.value, columns, rows);
307        }
308
309        // Execute the main query with CTE registry
310        self.execute_main_query_with_ctes(stmt, ctx, &mut cte_registry)
311    }
312
313    /// Execute a single CTE query
314    fn execute_cte_query(
315        &self,
316        stmt: &SelectStatement,
317        ctx: &ExecutionContext,
318        cte_registry: &mut CteRegistry,
319    ) -> Result<(Vec<String>, RowVec)> {
320        let ctx_with_ctes = ctx.with_cte_data(cte_registry.data());
321        let mut statement = stmt.clone();
322        statement.with = None;
323        let result = self.host.cte_execute_select(&statement, &ctx_with_ctes)?;
324        let columns = result.columns().to_vec();
325        let rows = materialize_result(result)?;
326
327        Ok((columns, rows))
328    }
329
330    /// Execute a recursive CTE
331    fn execute_recursive_cte_with_columns(
332        &self,
333        cte_name: &str,
334        stmt: &SelectStatement,
335        ctx: &ExecutionContext,
336        cte_registry: &mut CteRegistry,
337        column_aliases: Option<&[Identifier]>,
338    ) -> Result<(Vec<String>, RowVec)> {
339        use radixdb_sql::ast::SetOperationType;
340
341        // Maximum iterations to prevent infinite loops
342        const MAX_ITERATIONS: usize = 10000;
343
344        // The recursive CTE query should have UNION ALL structure
345        if stmt.set_operations.is_empty() {
346            return Err(Error::InvalidArgument(
347                "Recursive CTE must have UNION ALL between anchor and recursive members"
348                    .to_string(),
349            ));
350        }
351
352        // Check that all set operations are UNION ALL
353        for set_op in &stmt.set_operations {
354            if !matches!(set_op.operation, SetOperationType::UnionAll) {
355                return Err(Error::InvalidArgument(
356                    "Recursive CTE only supports UNION ALL (not UNION)".to_string(),
357                ));
358            }
359        }
360
361        // Execute the anchor member (the first SELECT before UNION ALL)
362        let anchor_stmt = SelectStatement {
363            token: stmt.token.clone(),
364            distinct: stmt.distinct,
365            distinct_on: stmt.distinct_on.clone(),
366            columns: stmt.columns.clone(),
367            with: None,
368            table_expr: stmt.table_expr.clone(),
369            where_clause: stmt.where_clause.clone(),
370            group_by: stmt.group_by.clone(),
371            having: stmt.having.clone(),
372            window_defs: stmt.window_defs.clone(),
373            order_by: vec![], // No ORDER BY for anchor
374            limit: None,
375            offset: None,
376            set_operations: vec![],
377        };
378
379        let result = self.host.cte_execute_select(&anchor_stmt, ctx)?;
380        let anchor_columns = result.columns().to_vec();
381
382        if let Some(aliases) = column_aliases {
383            if aliases.len() != anchor_columns.len() {
384                return Err(Error::InvalidArgument(format!(
385                    "recursive CTE {cte_name} declares {} columns but anchor returns {}",
386                    aliases.len(),
387                    anchor_columns.len()
388                )));
389            }
390        }
391
392        // Apply column aliases if provided (for recursive CTE column naming)
393        let columns: Vec<String> = if let Some(aliases) = column_aliases {
394            aliases
395                .iter()
396                .enumerate()
397                .map(|(i, alias)| {
398                    if i < anchor_columns.len() {
399                        alias.value.to_string()
400                    } else {
401                        anchor_columns
402                            .get(i)
403                            .cloned()
404                            .unwrap_or_else(|| format!("col{}", i))
405                    }
406                })
407                .collect()
408        } else {
409            anchor_columns
410        };
411
412        let mut all_rows = materialize_result(result)?;
413        if all_rows.iter().any(|(_, row)| row.len() != columns.len()) {
414            return Err(Error::InvalidArgument(format!(
415                "recursive CTE {cte_name} anchor row width does not match its {} columns",
416                columns.len()
417            )));
418        }
419
420        // If no anchor rows, return empty result
421        if all_rows.is_empty() {
422            return Ok((columns, all_rows));
423        }
424
425        let mut target_types = vec![DataType::Null; columns.len()];
426        for (_, row) in all_rows.iter() {
427            for (column, value) in row.iter().enumerate() {
428                target_types[column] = merge_set_type(target_types[column], value.data_type())
429                    .map_err(|error| {
430                        Error::InvalidArgument(format!(
431                            "recursive CTE {cte_name} anchor types are incompatible: {error}"
432                        ))
433                    })?;
434            }
435        }
436        for (_, row) in all_rows.iter_mut() {
437            for (value, target_type) in row.iter_mut().zip(&target_types) {
438                if value.data_type() != *target_type {
439                    *value = value.try_coerce_to_type(*target_type).map_err(|error| {
440                        Error::InvalidArgument(format!(
441                            "recursive CTE {cte_name} anchor type is incompatible: {error}"
442                        ))
443                    })?;
444                }
445            }
446        }
447
448        // Current working set (rows from previous iteration)
449        let mut retained = RetainedRowsBudget::new("recursive CTE");
450        for (_, row) in all_rows.iter() {
451            retained.admit(row)?; // accumulated result
452            retained.admit(row)?; // first working set clone
453        }
454        let mut working_rows = all_rows.clone();
455
456        // Iterate until no new rows or max iterations
457        let mut converged = false;
458        for _iteration in 0..MAX_ITERATIONS {
459            ctx.check_cancelled()?;
460            if working_rows.is_empty() {
461                converged = true;
462                break;
463            }
464
465            // Create a temporary CTE registry with current working set
466            let mut temp_registry = CteRegistry::new();
467            // Copy existing CTEs - share Arc to avoid cloning row data
468            for (name, (cols, rows, materialized_rows)) in cte_registry.iter() {
469                temp_registry.store_arc(
470                    name,
471                    cols.clone(),
472                    CompactArc::clone(rows),
473                    Arc::clone(materialized_rows),
474                );
475            }
476            // Move the working set behind a shared owner. The recursive query
477            // borrows the Arc and no third full row clone is created.
478            let working_rows_arc = CompactArc::new(working_rows.into_iter().collect());
479            temp_registry.store_arc(
480                cte_name,
481                CompactArc::new(columns.clone()),
482                CompactArc::clone(&working_rows_arc),
483                Arc::new(OnceLock::new()),
484            );
485
486            // Execute each recursive member
487            let mut new_rows = RowVec::new();
488            for set_op in &stmt.set_operations {
489                // The recursive member is in set_op.right
490                let mut recursive_result =
491                    self.execute_cte_query(&set_op.right, ctx, &mut temp_registry)?;
492
493                if recursive_result.0.len() != columns.len() {
494                    return Err(Error::InvalidArgument(format!(
495                        "recursive CTE {cte_name} member returns {} columns but anchor returns {}",
496                        recursive_result.0.len(),
497                        columns.len()
498                    )));
499                }
500
501                let mut merged_types = target_types.clone();
502                for (_, row) in recursive_result.1.iter() {
503                    if row.len() != columns.len() {
504                        return Err(Error::InvalidArgument(format!(
505                            "recursive CTE {cte_name} member row width {} does not match {}",
506                            row.len(),
507                            columns.len()
508                        )));
509                    }
510                    for (column, value) in row.iter().enumerate() {
511                        merged_types[column] =
512                            merge_set_type(merged_types[column], value.data_type()).map_err(
513                                |error| {
514                                    Error::InvalidArgument(format!(
515                                "recursive CTE {cte_name} member type is incompatible: {error}"
516                            ))
517                                },
518                            )?;
519                    }
520                }
521
522                if merged_types != target_types {
523                    for (_, row) in all_rows.iter_mut().chain(new_rows.iter_mut()) {
524                        for (value, target_type) in row.iter_mut().zip(&merged_types) {
525                            if value.data_type() != *target_type {
526                                *value =
527                                    value.try_coerce_to_type(*target_type).map_err(|error| {
528                                        Error::InvalidArgument(format!(
529                                        "recursive CTE {cte_name} type migration failed: {error}"
530                                    ))
531                                    })?;
532                            }
533                        }
534                    }
535                    target_types = merged_types;
536                }
537
538                // Extend with rows from recursive result, renumbering row IDs
539                let base_id = new_rows.len() as i64;
540                for (i, (_, mut row)) in recursive_result.1.drain(..).enumerate() {
541                    if i & 0xff == 0 {
542                        ctx.check_cancelled()?;
543                    }
544                    if row.len() != columns.len() {
545                        return Err(Error::InvalidArgument(format!(
546                            "recursive CTE {cte_name} member row width {} does not match {}",
547                            row.len(),
548                            columns.len()
549                        )));
550                    }
551                    for (value, target_type) in row.iter_mut().zip(&target_types) {
552                        if value.data_type() != *target_type {
553                            *value = value.try_coerce_to_type(*target_type).map_err(|error| {
554                                Error::InvalidArgument(format!(
555                                    "recursive CTE {cte_name} member type is incompatible with anchor: {error}"
556                                ))
557                            })?;
558                        }
559                    }
560                    retained.admit(&row)?;
561                    new_rows.push((base_id + i as i64, row));
562                }
563            }
564
565            // The previous working set is no longer retained separately from
566            // `all_rows` after the recursive members finish.
567            drop(temp_registry);
568            for (_, row) in working_rows_arc.iter() {
569                retained.release(row);
570            }
571
572            if new_rows.is_empty() {
573                converged = true;
574                break;
575            }
576
577            // Add new rows to total result, renumbering row IDs
578            let base_id = all_rows.len() as i64;
579            for (i, (_, row)) in new_rows.iter().enumerate() {
580                retained.admit(row)?;
581                all_rows.push((base_id + i as i64, row.clone()));
582            }
583
584            // New rows become the working set for next iteration
585            working_rows = new_rows;
586        }
587
588        if !converged {
589            return Err(Error::InvalidArgument(format!(
590                "recursive CTE {cte_name} exceeded {MAX_ITERATIONS} iterations"
591            )));
592        }
593
594        let classification = get_classification(stmt);
595        all_rows =
596            self.apply_order_by_limit_offset(stmt, ctx, &classification, all_rows, &columns)?;
597
598        Ok((columns, all_rows))
599    }
600
601    /// Execute the main query with CTEs available
602    fn execute_main_query_with_ctes(
603        &self,
604        stmt: &SelectStatement,
605        ctx: &ExecutionContext,
606        cte_registry: &mut CteRegistry,
607    ) -> Result<Box<dyn QueryResult>> {
608        let ctx_with_ctes = ctx.with_cte_data(cte_registry.data());
609        let mut statement = stmt.clone();
610        statement.with = None;
611        // CTE rows are regular in-memory table sources in the execution context.
612        // Running the ordinary SELECT pipeline keeps JOIN projection, set ops,
613        // DISTINCT, complex ORDER BY and final paging under the same owners.
614        self.host.cte_execute_select(&statement, &ctx_with_ctes)
615    }
616
617    /// Execute a query on CTE result data
618    #[allow(dead_code)]
619    pub(crate) fn execute_query_on_cte_result(
620        &self,
621        stmt: &SelectStatement,
622        ctx: &ExecutionContext,
623        cte_columns: Vec<String>,
624        cte_rows: RowVec,
625    ) -> Result<(Vec<String>, RowVec)> {
626        let (cols, rows, _applied) =
627            self.execute_query_on_cte_result_inner(stmt, ctx, cte_columns, cte_rows, false)?;
628        Ok((cols, rows))
629    }
630
631    /// Inner implementation that optionally skips ORDER BY/LIMIT processing.
632    /// Returns (columns, rows, order_limit_applied).
633    /// When `skip_order_limit` is true, ORDER BY and LIMIT/OFFSET are NOT applied,
634    /// allowing the caller to delegate to a more capable ORDER BY handler.
635    pub(crate) fn execute_query_on_cte_result_inner(
636        &self,
637        stmt: &SelectStatement,
638        ctx: &ExecutionContext,
639        cte_columns: Vec<String>,
640        cte_rows: RowVec,
641        skip_order_limit: bool,
642    ) -> Result<(Vec<String>, RowVec, bool)> {
643        // OPTIMIZATION: Get cached query classification to avoid repeated AST traversals
644        let classification = get_classification(stmt);
645
646        // Apply WHERE clause filter
647        let filtered_rows = if let Some(ref where_clause) = stmt.where_clause {
648            // Process subqueries in WHERE clause (e.g., IN subqueries on CTEs)
649            // Use cached classification to avoid AST traversal
650            let processed_where = if classification.where_has_subqueries {
651                self.host.process_where_subqueries(where_clause, ctx)?
652            } else {
653                (**where_clause).clone()
654            };
655
656            // Compile filter once and reuse for all rows
657            let mut eval = ExpressionEval::compile_with_options(
658                &processed_where,
659                &cte_columns,
660                None,
661                ctx.outer_columns(),
662                None,
663                self.host.cte_function_registry(),
664            )?
665            .with_context(ctx);
666
667            let mut result = RowVec::new();
668            let mut row_id = 0i64;
669            for (_, row) in cte_rows {
670                if eval.eval_bool_checked(&row)? {
671                    result.push((row_id, row));
672                    row_id += 1;
673                }
674            }
675            result
676        } else {
677            cte_rows
678        };
679
680        // Check for aggregation
681        if classification.has_aggregation {
682            let result = self.host.execute_select_with_aggregation(
683                stmt,
684                ctx,
685                filtered_rows,
686                &cte_columns,
687            )?;
688            let columns = result.columns().to_vec();
689            let mut rows = materialize_result(result)?;
690
691            if !skip_order_limit {
692                rows =
693                    self.apply_order_by_limit_offset(stmt, ctx, &classification, rows, &columns)?;
694            }
695            return Ok((columns, rows, !skip_order_limit));
696        }
697
698        // Check for window functions
699        if classification.has_window_functions {
700            let result = self.host.execute_select_with_window_functions(
701                stmt,
702                ctx,
703                &filtered_rows,
704                &cte_columns,
705            )?;
706            let columns = result.columns().to_vec();
707            let mut rows = materialize_result(result)?;
708
709            if !skip_order_limit {
710                rows =
711                    self.apply_order_by_limit_offset(stmt, ctx, &classification, rows, &columns)?;
712            }
713            return Ok((columns, rows, !skip_order_limit));
714        }
715
716        // Process scalar subqueries in SELECT columns before projection
717        let processed_columns = self
718            .host
719            .try_process_select_subqueries(&stmt.columns, ctx)?;
720        let columns_to_use = processed_columns.as_ref().unwrap_or(&stmt.columns);
721
722        // Determine output columns
723        let output_columns =
724            self.resolve_cte_output_columns_from_exprs(columns_to_use, &cte_columns)?;
725
726        let needs_projection = self.needs_projection_for_columns(columns_to_use);
727
728        if skip_order_limit {
729            // Caller will handle ORDER BY + LIMIT/OFFSET via expression-based sort.
730            // If ORDER BY references source columns not in the projected output,
731            // return unprojected rows so the caller can evaluate ORDER BY expressions.
732            // The caller's truncation logic (expected_columns) will trim afterwards.
733            let needs_source_for_order = classification.has_order_by
734                && self.order_by_needs_source_columns(
735                    &stmt.order_by,
736                    &output_columns,
737                    &cte_columns,
738                );
739
740            if needs_source_for_order {
741                // Return source columns + projected columns so caller can sort on source columns
742                // then trim to projected columns via expected_columns mechanism
743                let mut combined_columns = output_columns.clone();
744                for src_col in &cte_columns {
745                    if !combined_columns
746                        .iter()
747                        .any(|c| c.eq_ignore_ascii_case(src_col))
748                    {
749                        combined_columns.push(src_col.clone());
750                    }
751                }
752
753                // Build rows with projected columns first, then extra source columns
754                let result_rows = if needs_projection {
755                    let projected = self.project_cte_rows_from_columns(
756                        columns_to_use,
757                        &filtered_rows,
758                        &cte_columns,
759                        ctx,
760                    )?;
761                    // Append source columns that aren't in output
762                    let extra_src_indices: Vec<usize> = cte_columns
763                        .iter()
764                        .enumerate()
765                        .filter(|(_, c)| {
766                            !output_columns.iter().any(|oc| oc.eq_ignore_ascii_case(c))
767                        })
768                        .map(|(i, _)| i)
769                        .collect();
770
771                    projected
772                        .into_iter()
773                        .zip(filtered_rows.iter())
774                        .map(|((id, proj_row), (_, src_row))| {
775                            let mut vals = proj_row.into_values();
776                            for &idx in &extra_src_indices {
777                                vals.push(
778                                    src_row.get(idx).cloned().unwrap_or(Value::null_unknown()),
779                                );
780                            }
781                            (id, Row::from_values(vals))
782                        })
783                        .collect()
784                } else {
785                    filtered_rows
786                };
787                return Ok((combined_columns, result_rows, false));
788            }
789
790            let result_rows = if needs_projection {
791                self.project_cte_rows_from_columns(
792                    columns_to_use,
793                    &filtered_rows,
794                    &cte_columns,
795                    ctx,
796                )?
797            } else {
798                filtered_rows
799            };
800            return Ok((output_columns, result_rows, false));
801        }
802
803        // Check if ORDER BY references source columns not in the projected output.
804        // If so, sort BEFORE projection using source columns, then project after.
805        let needs_pre_sort = classification.has_order_by
806            && self.order_by_needs_source_columns(&stmt.order_by, &output_columns, &cte_columns);
807
808        // Sort before projection if ORDER BY references non-projected source columns
809        let mut result_rows = if needs_pre_sort {
810            if needs_projection {
811                // Sort on source columns first, then project
812                let sorted =
813                    self.apply_order_by_to_rows(filtered_rows, &stmt.order_by, &cte_columns)?;
814                self.project_cte_rows_from_columns(columns_to_use, &sorted, &cte_columns, ctx)?
815            } else {
816                self.apply_order_by_to_rows(filtered_rows, &stmt.order_by, &cte_columns)?
817            }
818        } else {
819            // Normal path: project first, then sort on output columns
820            let mut rows = if needs_projection {
821                self.project_cte_rows_from_columns(
822                    columns_to_use,
823                    &filtered_rows,
824                    &cte_columns,
825                    ctx,
826                )?
827            } else {
828                filtered_rows
829            };
830
831            if classification.has_order_by {
832                rows = self.apply_order_by_to_rows(rows, &stmt.order_by, &output_columns)?;
833            }
834            rows
835        };
836
837        // Apply LIMIT and OFFSET (using classification for quick check)
838        if classification.has_offset {
839            if let Some(ref offset_expr) = stmt.offset {
840                let offset = evaluate_page_expression(offset_expr, ctx, "OFFSET")?;
841                // Use drain to avoid extra allocation from skip().collect()
842                if offset > 0 && offset < result_rows.len() {
843                    result_rows.drain(..offset);
844                } else if offset >= result_rows.len() {
845                    result_rows.clear();
846                }
847            }
848        }
849
850        if classification.has_limit {
851            if let Some(ref limit_expr) = stmt.limit {
852                let limit = evaluate_page_expression(limit_expr, ctx, "LIMIT")?;
853                if limit < result_rows.len() {
854                    result_rows.truncate(limit);
855                }
856            }
857        }
858
859        Ok((output_columns, result_rows, true))
860    }
861
862    /// Extract the base CTE/table name for registry lookup (ignores aliases)
863    fn extract_cte_name_for_lookup(&self, expr: &Expression) -> Option<String> {
864        match expr {
865            Expression::CteReference(cte_ref) => Some(cte_ref.name.value.to_string()),
866            Expression::TableSource(simple_table_source) => {
867                Some(simple_table_source.name.value.to_string())
868            }
869            Expression::Identifier(id) => Some(id.value.to_string()),
870            _ => None,
871        }
872    }
873
874    /// Resolve output column names from expression list (for processed subqueries)
875    fn resolve_cte_output_columns_from_exprs(
876        &self,
877        columns: &[Expression],
878        cte_columns: &[String],
879    ) -> Result<Vec<String>> {
880        let mut output_columns = Vec::new();
881
882        for (i, col_expr) in columns.iter().enumerate() {
883            match col_expr {
884                Expression::Star(_) | Expression::QualifiedStar(_) => {
885                    output_columns.extend(cte_columns.iter().cloned());
886                }
887                Expression::Identifier(id) => {
888                    output_columns.push(id.value.to_string());
889                }
890                Expression::Aliased(aliased) => {
891                    output_columns.push(aliased.alias.value.to_string());
892                }
893                _ => {
894                    output_columns.push(format!("expr{}", i + 1));
895                }
896            }
897        }
898
899        if output_columns.is_empty() {
900            output_columns = cte_columns.to_vec();
901        }
902
903        Ok(output_columns)
904    }
905
906    /// Check if columns need projection
907    fn needs_projection_for_columns(&self, columns: &[Expression]) -> bool {
908        if columns.is_empty() {
909            return false;
910        }
911
912        // Check if it's just SELECT *
913        if columns.len() == 1 {
914            if let Expression::Star(_) = &columns[0] {
915                return false;
916            }
917        }
918
919        true
920    }
921
922    /// Project rows based on provided column expressions (for processed subqueries)
923    fn project_cte_rows_from_columns(
924        &self,
925        columns: &[Expression],
926        rows: &RowVec,
927        cte_columns: &[String],
928        ctx: &ExecutionContext,
929    ) -> Result<RowVec> {
930        use super::expression::{ExecuteContext, ExprVM, SharedProgram};
931
932        let col_index_map = build_column_index_map(cte_columns);
933
934        // Pre-compile expressions that need evaluation
935        // Store: Star -> None, Identifier -> column index, Complex -> compiled program
936        enum CompiledColumn {
937            Star,
938            Identifier(usize),
939            Compiled(SharedProgram),
940        }
941
942        let compiled_columns: Vec<CompiledColumn> = columns
943            .iter()
944            .map(|col_expr| match col_expr {
945                Expression::Star(_) => Ok(CompiledColumn::Star),
946                Expression::Identifier(id) => {
947                    let idx = col_index_map
948                        .get(id.value_lower.as_str())
949                        .copied()
950                        .ok_or_else(|| Error::ColumnNotFound(id.value.to_string()))?;
951                    Ok(CompiledColumn::Identifier(idx))
952                }
953                Expression::Aliased(aliased) => {
954                    let program = compile_expression_with_context(
955                        &aliased.expression,
956                        cte_columns,
957                        ctx.outer_columns(),
958                        self.host.cte_function_registry(),
959                    )?;
960                    Ok(CompiledColumn::Compiled(program))
961                }
962                _ => {
963                    let program = compile_expression_with_context(
964                        col_expr,
965                        cte_columns,
966                        ctx.outer_columns(),
967                        self.host.cte_function_registry(),
968                    )?;
969                    Ok(CompiledColumn::Compiled(program))
970                }
971            })
972            .collect::<Result<Vec<_>>>()?;
973
974        // Create VM for expression execution (reused for all rows)
975        let mut vm = ExprVM::new();
976        let mut result_rows = RowVec::with_capacity(rows.len());
977
978        for (row_id, (_, row)) in rows.iter().enumerate() {
979            // OPTIMIZATION: Pre-allocate CompactVec with estimated capacity
980            let mut values: CompactVec<Value> =
981                CompactVec::with_capacity(columns.len().max(row.len()));
982            // CRITICAL: Include params from context for parameterized queries
983            let mut exec_ctx = ExecuteContext::new(row)
984                .with_params(ctx.params())
985                .with_named_params(ctx.named_params())
986                .with_transaction_id(ctx.transaction_id())
987                .with_stored_function_invoker(ctx.stored_function_invoker());
988            if let Some(outer_row) = ctx.outer_row() {
989                exec_ctx = exec_ctx.with_outer_row(outer_row);
990            }
991
992            for compiled in &compiled_columns {
993                match compiled {
994                    CompiledColumn::Star => {
995                        // OPTIMIZATION: Extend with row values
996                        values.extend(row.iter().cloned());
997                    }
998                    CompiledColumn::Identifier(idx) => {
999                        values.push(row.get(*idx).cloned().unwrap_or_else(Value::null_unknown));
1000                    }
1001                    CompiledColumn::Compiled(program) => {
1002                        values.push(vm.execute_cow(program, &exec_ctx)?);
1003                    }
1004                }
1005            }
1006
1007            result_rows.push((row_id as i64, Row::from_compact_vec(values)));
1008        }
1009
1010        Ok(result_rows)
1011    }
1012
1013    /// Check if a SELECT statement has a WITH clause
1014    pub(crate) fn has_cte(&self, stmt: &SelectStatement) -> bool {
1015        stmt.with.is_some()
1016    }
1017
1018    /// Apply ORDER BY to rows
1019    fn apply_order_by_to_rows(
1020        &self,
1021        mut rows: RowVec,
1022        order_by: &[radixdb_sql::ast::OrderByExpression],
1023        columns: &[String],
1024    ) -> Result<RowVec> {
1025        if order_by.is_empty() || rows.is_empty() {
1026            return Ok(rows);
1027        }
1028
1029        // Build column index map
1030        let col_index_map = build_column_index_map(columns);
1031
1032        // Build order specs: (column_index, ascending, nulls_first)
1033        let order_specs: Vec<(Option<usize>, bool, Option<bool>)> = order_by
1034            .iter()
1035            .map(|ob| {
1036                let col_idx = match &ob.expression {
1037                    Expression::Identifier(id) => {
1038                        col_index_map.get(id.value_lower.as_str()).copied()
1039                    }
1040                    Expression::QualifiedIdentifier(qi) => {
1041                        // Try both qualified and unqualified names
1042                        let full_name =
1043                            format!("{}.{}", qi.qualifier, qi.name.value).to_lowercase();
1044                        col_index_map
1045                            .get(&full_name)
1046                            .or_else(|| col_index_map.get(qi.name.value_lower.as_str()))
1047                            .copied()
1048                    }
1049                    Expression::IntegerLiteral(lit) => {
1050                        // ORDER BY 1, 2, etc. - 1-based column position
1051                        let pos = lit.value as usize;
1052                        if pos > 0 && pos <= columns.len() {
1053                            Some(pos - 1)
1054                        } else {
1055                            None
1056                        }
1057                    }
1058                    _ => None,
1059                };
1060                (col_idx, ob.ascending, ob.nulls_first)
1061            })
1062            .collect();
1063
1064        // Sort using the same comparison function as the main query executor
1065        // RowVec derefs to Vec<(i64, Row)>, so we sort by the Row part
1066        rows.sort_by(|(_, a), (_, b)| {
1067            for (col_idx, ascending, nulls_first) in &order_specs {
1068                if let Some(idx) = col_idx {
1069                    let a_val = a.get(*idx);
1070                    let b_val = b.get(*idx);
1071
1072                    // Check if either value is NULL
1073                    let a_is_null = a_val.is_none() || a_val.map(|v| v.is_null()).unwrap_or(true);
1074                    let b_is_null = b_val.is_none() || b_val.map(|v| v.is_null()).unwrap_or(true);
1075
1076                    // Handle NULL comparison
1077                    if a_is_null || b_is_null {
1078                        if a_is_null && b_is_null {
1079                            continue; // Both NULL, move to next column
1080                        }
1081                        // Default: NULLS LAST for ASC, NULLS FIRST for DESC
1082                        let nulls_come_first = nulls_first.unwrap_or(!*ascending);
1083                        let cmp = if a_is_null {
1084                            if nulls_come_first {
1085                                std::cmp::Ordering::Less
1086                            } else {
1087                                std::cmp::Ordering::Greater
1088                            }
1089                        } else if nulls_come_first {
1090                            std::cmp::Ordering::Greater
1091                        } else {
1092                            std::cmp::Ordering::Less
1093                        };
1094                        return cmp;
1095                    }
1096
1097                    // Both non-NULL - normal comparison
1098                    let cmp = match (a_val, b_val) {
1099                        (Some(av), Some(bv)) => {
1100                            av.partial_cmp(bv).unwrap_or(std::cmp::Ordering::Equal)
1101                        }
1102                        _ => std::cmp::Ordering::Equal,
1103                    };
1104
1105                    let cmp = if !*ascending { cmp.reverse() } else { cmp };
1106
1107                    if cmp != std::cmp::Ordering::Equal {
1108                        return cmp;
1109                    }
1110                }
1111            }
1112            std::cmp::Ordering::Equal
1113        });
1114
1115        Ok(rows)
1116    }
1117
1118    /// Check if ORDER BY references source columns not present in the projected output.
1119    /// Returns true if sorting must happen before projection.
1120    /// Recursively inspects expressions (e.g., -value, value*2) for column references.
1121    fn order_by_needs_source_columns(
1122        &self,
1123        order_by: &[OrderByExpression],
1124        output_columns: &[String],
1125        source_columns: &[String],
1126    ) -> bool {
1127        let output_lower: AHashSet<String> =
1128            output_columns.iter().map(|c| c.to_lowercase()).collect();
1129
1130        for ob in order_by {
1131            if self.expr_references_source_not_output(&ob.expression, &output_lower, source_columns)
1132            {
1133                return true;
1134            }
1135        }
1136        false
1137    }
1138
1139    /// Recursively check if an expression references source columns not in the output.
1140    /// Conservative: returns true for any unknown composite expression to avoid
1141    /// silently dropping columns that ORDER BY needs.
1142    fn expr_references_source_not_output(
1143        &self,
1144        expr: &Expression,
1145        output_lower: &AHashSet<String>,
1146        source_columns: &[String],
1147    ) -> bool {
1148        let check = |e: &Expression| {
1149            self.expr_references_source_not_output(e, output_lower, source_columns)
1150        };
1151        let is_source_not_output = |name: &str| {
1152            !output_lower.contains(name)
1153                && source_columns.iter().any(|c| c.eq_ignore_ascii_case(name))
1154        };
1155
1156        match expr {
1157            // Leaf column references — the core check
1158            Expression::Identifier(id) => is_source_not_output(id.value_lower.as_str()),
1159            Expression::QualifiedIdentifier(qi) => {
1160                is_source_not_output(qi.name.value_lower.as_str())
1161            }
1162
1163            // Literals and constants — never reference columns
1164            Expression::IntegerLiteral(_)
1165            | Expression::FloatLiteral(_)
1166            | Expression::StringLiteral(_)
1167            | Expression::BooleanLiteral(_)
1168            | Expression::NullLiteral(_)
1169            | Expression::IntervalLiteral(_)
1170            | Expression::Parameter(_)
1171            | Expression::Star(_)
1172            | Expression::QualifiedStar(_)
1173            | Expression::Default(_) => false,
1174
1175            // Composite expressions — recurse into children
1176            Expression::Prefix(p) => check(&p.right),
1177            Expression::Infix(inf) => check(&inf.left) || check(&inf.right),
1178            Expression::FunctionCall(fc) => fc.arguments.iter().any(&check),
1179            Expression::Cast(c) => check(&c.expr),
1180            Expression::Aliased(a) => check(&a.expression),
1181            Expression::Case(case) => {
1182                case.value.as_ref().is_some_and(|v| check(v))
1183                    || case
1184                        .when_clauses
1185                        .iter()
1186                        .any(|w| check(&w.condition) || check(&w.then_result))
1187                    || case.else_value.as_ref().is_some_and(|e| check(e))
1188            }
1189            Expression::Between(b) => check(&b.expr) || check(&b.lower) || check(&b.upper),
1190            Expression::In(i) => check(&i.left),
1191            Expression::Like(l) => check(&l.left) || check(&l.pattern),
1192            Expression::Distinct(d) => check(&d.expr),
1193            Expression::Window(w) => w.function.arguments.iter().any(check),
1194
1195            // Unknown composite — conservatively assume it may reference source columns
1196            _ => true,
1197        }
1198    }
1199
1200    /// Apply ORDER BY, OFFSET, and LIMIT to in-memory rows.
1201    /// Used by aggregation and window function paths in execute_query_on_cte_result
1202    /// which otherwise early-return without these post-processing steps.
1203    fn apply_order_by_limit_offset(
1204        &self,
1205        stmt: &SelectStatement,
1206        ctx: &ExecutionContext,
1207        classification: &QueryClassification,
1208        mut rows: RowVec,
1209        columns: &[String],
1210    ) -> Result<RowVec> {
1211        if classification.has_order_by {
1212            rows = self.apply_order_by_to_rows(rows, &stmt.order_by, columns)?;
1213        }
1214
1215        if classification.has_offset {
1216            if let Some(ref offset_expr) = stmt.offset {
1217                let offset = evaluate_page_expression(offset_expr, ctx, "OFFSET")?;
1218                if offset > 0 && offset < rows.len() {
1219                    rows.drain(..offset);
1220                } else if offset >= rows.len() {
1221                    rows.clear();
1222                }
1223            }
1224        }
1225
1226        if classification.has_limit {
1227            if let Some(ref limit_expr) = stmt.limit {
1228                let limit = evaluate_page_expression(limit_expr, ctx, "LIMIT")?;
1229                if limit < rows.len() {
1230                    rows.truncate(limit);
1231                }
1232            }
1233        }
1234
1235        Ok(rows)
1236    }
1237
1238    // =========================================================================
1239    // CTE INLINING OPTIMIZATION
1240    // =========================================================================
1241
1242    /// Check if LIMIT pushdown would be more beneficial than CTE inlining.
1243    ///
1244    /// Returns true when streaming aggregation with limit pushdown will be faster
1245    /// than inlining as a subquery.
1246    fn should_use_limit_pushdown_instead(
1247        &self,
1248        stmt: &SelectStatement,
1249        with_clause: &WithClause,
1250    ) -> bool {
1251        // Must have LIMIT without ORDER BY
1252        if stmt.limit.is_none() || !stmt.order_by.is_empty() {
1253            return false;
1254        }
1255
1256        // Must have a JOIN
1257        let join_source = match &stmt.table_expr {
1258            Some(expr) => match expr.as_ref() {
1259                Expression::JoinSource(js) => js,
1260                _ => return false,
1261            },
1262            None => return false,
1263        };
1264
1265        let join_type = join_source.join_type.to_uppercase();
1266        let is_inner_join = join_type == "INNER" || join_type.is_empty() || join_type == "JOIN";
1267        let is_left_join = join_type == "LEFT" || join_type == "LEFT OUTER";
1268        let is_right_join = join_type == "RIGHT" || join_type == "RIGHT OUTER";
1269
1270        if !is_inner_join && !is_left_join && !is_right_join {
1271            return false;
1272        }
1273
1274        // Check if exactly one side is a CTE with GROUP BY
1275        let cte_names: AHashSet<String> = with_clause
1276            .ctes
1277            .iter()
1278            .filter(|c| !c.is_recursive && !c.query.group_by.columns.is_empty())
1279            .map(|c| c.name.value_lower.to_string())
1280            .collect();
1281
1282        if cte_names.is_empty() {
1283            return false;
1284        }
1285
1286        let left_cte = self
1287            .extract_cte_name_for_lookup(&join_source.left)
1288            .filter(|n| cte_names.contains(&n.to_lowercase()));
1289        let right_cte = self
1290            .extract_cte_name_for_lookup(&join_source.right)
1291            .filter(|n| cte_names.contains(&n.to_lowercase()));
1292
1293        // For INNER JOIN: either side can be CTE
1294        // For LEFT JOIN: CTE must be on the RIGHT (each CTE row produces at most one result)
1295        // For RIGHT JOIN: CTE must be on the LEFT (each CTE row produces at most one result)
1296        match (&left_cte, &right_cte) {
1297            (Some(_), None) if is_inner_join || is_right_join => true,
1298            (None, Some(_)) if is_inner_join || is_left_join => true,
1299            _ => false,
1300        }
1301    }
1302
1303    /// Try to inline single-use, non-recursive CTEs as subqueries.
1304    /// Returns Some(rewritten_stmt) if all CTEs can be inlined, None otherwise.
1305    ///
1306    /// Benefits of inlining:
1307    /// - Preserves index access (materialized CTEs lose all indexes)
1308    /// - Enables LIMIT pushdown through subqueries
1309    /// - Avoids memory overhead of full CTE materialization
1310    pub(crate) fn try_inline_ctes(
1311        &self,
1312        stmt: &SelectStatement,
1313        with_clause: &WithClause,
1314    ) -> Option<SelectStatement> {
1315        // Inlining removes WITH from the rewritten statement, so it is only
1316        // admitted for the one shape whose complete reference graph is the
1317        // FROM item itself. Rich expressions and compound branches stay
1318        // materialized until their full AST dependency graph is proven.
1319        let simple_projection = stmt.columns.len() == 1
1320            && matches!(
1321                stmt.columns[0],
1322                Expression::Star(_) | Expression::QualifiedStar(_)
1323            );
1324        if !simple_projection
1325            || stmt.where_clause.is_some()
1326            || stmt.having.is_some()
1327            || !stmt.group_by.columns.is_empty()
1328            || !stmt.window_defs.is_empty()
1329            || !stmt.order_by.is_empty()
1330            || stmt.limit.is_some()
1331            || stmt.offset.is_some()
1332            || !stmt.set_operations.is_empty()
1333            || stmt.distinct
1334            || !stmt.distinct_on.is_empty()
1335        {
1336            return None;
1337        }
1338
1339        // Early exit: no table expression means nothing to inline
1340        let table_expr = stmt.table_expr.as_ref()?;
1341
1342        // Skip inlining if LIMIT pushdown with streaming would be more beneficial.
1343        // This happens when:
1344        // 1. Main query has LIMIT (no ORDER BY)
1345        // 2. CTE has GROUP BY (can use streaming aggregation)
1346        // 3. CTE is in INNER JOIN (limit pushdown is safe)
1347        if self.should_use_limit_pushdown_instead(stmt, with_clause) {
1348            return None;
1349        }
1350
1351        // Pre-compute lowercase CTE names once to avoid repeated to_lowercase() calls
1352        // Store (lowercase_name, original_cte) pairs
1353        let cte_names_lower: Vec<(String, &CommonTableExpression)> = with_clause
1354            .ctes
1355            .iter()
1356            .map(|cte| {
1357                // Early exit for conditions that prevent inlining
1358                if cte.is_recursive || !cte.column_names.is_empty() {
1359                    return Err(());
1360                }
1361                Ok((cte.name.value_lower.to_string(), cte))
1362            })
1363            .collect::<std::result::Result<Vec<_>, _>>()
1364            .ok()?;
1365
1366        // Build map from pre-computed lowercase names
1367        let cte_defs: StringMap<&CommonTableExpression> = cte_names_lower.iter().cloned().collect();
1368        let cte_name_set: AHashSet<&str> = cte_defs.keys().map(|s| s.as_str()).collect();
1369
1370        // Check if any CTE references another CTE (CTE chaining)
1371        // These cannot be simply inlined as they have data dependencies
1372        for (_, cte) in &cte_names_lower {
1373            for other_cte_name in &cte_name_set {
1374                if self.query_references_cte(&cte.query, other_cte_name) {
1375                    // CTE references another CTE - can't inline
1376                    return None;
1377                }
1378            }
1379        }
1380
1381        // Count CTE references in the main query using pre-computed names
1382        // Separate counts for table expressions (JOIN targets) vs WHERE clause subqueries
1383        let mut table_ref_counts: StringMap<usize> =
1384            cte_defs.keys().map(|name| (name.clone(), 0)).collect();
1385        let mut where_ref_counts: StringMap<usize> = table_ref_counts.clone();
1386
1387        // Count references in table expression (FROM/JOIN)
1388        self.count_cte_references_in_expr(table_expr, &mut table_ref_counts);
1389
1390        // Count references in WHERE clause (including IN/EXISTS subqueries)
1391        if let Some(ref where_clause) = stmt.where_clause {
1392            self.count_cte_references_in_expr(where_clause, &mut where_ref_counts);
1393        }
1394
1395        // Only inline CTEs that:
1396        // 1. Are used exactly once in table expressions (FROM/JOIN)
1397        // 2. Are NOT used in WHERE clause subqueries (these need special handling)
1398        for name in cte_defs.keys() {
1399            let table_refs = table_ref_counts.get(name).copied().unwrap_or(0);
1400            let where_refs = where_ref_counts.get(name).copied().unwrap_or(0);
1401
1402            // Skip if used in WHERE clause - subquery handling is different
1403            if where_refs > 0 {
1404                return None;
1405            }
1406
1407            // Skip if used more than once - multi-use benefits from materialization
1408            if table_refs > 1 {
1409                return None;
1410            }
1411            // table_refs == 0 (unused) or table_refs == 1 (single-use) can be inlined
1412        }
1413
1414        // All CTEs are single-use - perform inlining
1415        // OPTIMIZATION: Only clone if something actually changes
1416        // First check if any CTE references exist that we can inline
1417        let any_refs = table_ref_counts.values().any(|&count| count > 0);
1418        if !any_refs {
1419            // No CTE references in table expression - no inlining needed
1420            return None;
1421        }
1422
1423        // Try to inline - if nothing changes, skip the expensive cloning
1424        let inlined_expr = self.try_inline_cte_references(table_expr, &cte_defs)?;
1425
1426        Some(SelectStatement {
1427            token: stmt.token.clone(),
1428            distinct: stmt.distinct,
1429            distinct_on: stmt.distinct_on.clone(),
1430            columns: stmt.columns.clone(),
1431            with: None, // Remove WITH clause
1432            table_expr: Some(Box::new(inlined_expr)),
1433            where_clause: stmt.where_clause.clone(),
1434            group_by: stmt.group_by.clone(),
1435            having: stmt.having.clone(),
1436            window_defs: stmt.window_defs.clone(),
1437            order_by: stmt.order_by.clone(),
1438            limit: stmt.limit.clone(),
1439            offset: stmt.offset.clone(),
1440            set_operations: stmt.set_operations.clone(),
1441        })
1442    }
1443
1444    /// Check if a query references a specific CTE by name
1445    fn query_references_cte(&self, stmt: &SelectStatement, cte_name: &str) -> bool {
1446        // Check table expression
1447        if let Some(ref table_expr) = stmt.table_expr {
1448            if self.expr_references_cte(table_expr, cte_name) {
1449                return true;
1450            }
1451        }
1452
1453        // Check WHERE clause
1454        if let Some(ref where_clause) = stmt.where_clause {
1455            if self.expr_references_cte(where_clause, cte_name) {
1456                return true;
1457            }
1458        }
1459
1460        false
1461    }
1462
1463    /// Check if an expression references a CTE
1464    fn expr_references_cte(&self, expr: &Expression, cte_name: &str) -> bool {
1465        match expr {
1466            Expression::CteReference(cte_ref) => cte_ref.name.value.eq_ignore_ascii_case(cte_name),
1467            Expression::TableSource(ts) => ts.name.value.eq_ignore_ascii_case(cte_name),
1468            Expression::Identifier(id) => id.value.eq_ignore_ascii_case(cte_name),
1469            Expression::JoinSource(js) => {
1470                self.expr_references_cte(&js.left, cte_name)
1471                    || self.expr_references_cte(&js.right, cte_name)
1472            }
1473            Expression::SubquerySource(sq) => self.query_references_cte(&sq.subquery, cte_name),
1474            Expression::ScalarSubquery(sq) => self.query_references_cte(&sq.subquery, cte_name),
1475            Expression::In(in_expr) => {
1476                // Check if right side is a ScalarSubquery
1477                if let Expression::ScalarSubquery(sq) = &*in_expr.right {
1478                    self.query_references_cte(&sq.subquery, cte_name)
1479                } else {
1480                    false
1481                }
1482            }
1483            Expression::Exists(ex) => self.query_references_cte(&ex.subquery, cte_name),
1484            Expression::Infix(infix) => {
1485                self.expr_references_cte(&infix.left, cte_name)
1486                    || self.expr_references_cte(&infix.right, cte_name)
1487            }
1488            _ => false,
1489        }
1490    }
1491
1492    /// Count CTE references in a SELECT statement
1493    fn count_cte_references_in_stmt(
1494        &self,
1495        stmt: &SelectStatement,
1496        ref_counts: &mut StringMap<usize>,
1497    ) {
1498        // Check table expression
1499        if let Some(ref table_expr) = stmt.table_expr {
1500            self.count_cte_references_in_expr(table_expr, ref_counts);
1501        }
1502
1503        // Check WHERE clause
1504        if let Some(ref where_clause) = stmt.where_clause {
1505            self.count_cte_references_in_expr(where_clause, ref_counts);
1506        }
1507
1508        // Check SELECT columns for subqueries
1509        for col in &stmt.columns {
1510            self.count_cte_references_in_expr(col, ref_counts);
1511        }
1512    }
1513
1514    /// Count CTE references in an expression
1515    fn count_cte_references_in_expr(&self, expr: &Expression, ref_counts: &mut StringMap<usize>) {
1516        match expr {
1517            Expression::CteReference(cte_ref) => {
1518                let name: &str = cte_ref.name.value_lower.as_str();
1519                if let Some(count) = ref_counts.get_mut(name) {
1520                    *count += 1;
1521                }
1522            }
1523            Expression::TableSource(ts) => {
1524                let name: &str = ts.name.value_lower.as_str();
1525                if let Some(count) = ref_counts.get_mut(name) {
1526                    *count += 1;
1527                }
1528            }
1529            Expression::Identifier(id) => {
1530                let name: &str = id.value_lower.as_str();
1531                if let Some(count) = ref_counts.get_mut(name) {
1532                    *count += 1;
1533                }
1534            }
1535            Expression::JoinSource(js) => {
1536                self.count_cte_references_in_expr(&js.left, ref_counts);
1537                self.count_cte_references_in_expr(&js.right, ref_counts);
1538            }
1539            Expression::SubquerySource(sq) => {
1540                self.count_cte_references_in_stmt(&sq.subquery, ref_counts);
1541            }
1542            Expression::ScalarSubquery(sq) => {
1543                self.count_cte_references_in_stmt(&sq.subquery, ref_counts);
1544            }
1545            Expression::In(in_expr) => {
1546                self.count_cte_references_in_expr(&in_expr.left, ref_counts);
1547                // Check if right side is a ScalarSubquery
1548                if let Expression::ScalarSubquery(sq) = &*in_expr.right {
1549                    self.count_cte_references_in_stmt(&sq.subquery, ref_counts);
1550                }
1551            }
1552            Expression::Exists(ex) => {
1553                self.count_cte_references_in_stmt(&ex.subquery, ref_counts);
1554            }
1555            Expression::Infix(infix) => {
1556                self.count_cte_references_in_expr(&infix.left, ref_counts);
1557                self.count_cte_references_in_expr(&infix.right, ref_counts);
1558            }
1559            Expression::Aliased(aliased) => {
1560                self.count_cte_references_in_expr(&aliased.expression, ref_counts);
1561            }
1562            _ => {}
1563        }
1564    }
1565
1566    /// Replace CTE references with subqueries in an expression.
1567    /// Returns Some(new_expr) if any replacement was made, None if no changes needed.
1568    fn try_inline_cte_references(
1569        &self,
1570        expr: &Expression,
1571        cte_defs: &StringMap<&CommonTableExpression>,
1572    ) -> Option<Expression> {
1573        match expr {
1574            Expression::CteReference(cte_ref) => {
1575                // Use pre-computed lowercase from value_lower if available
1576                let name = &cte_ref.name.value_lower;
1577                cte_defs.get(name.as_str()).map(|cte| {
1578                    // Convert CTE to SubquerySource
1579                    let alias = cte_ref
1580                        .alias
1581                        .clone()
1582                        .unwrap_or_else(|| cte_ref.name.clone());
1583                    Expression::SubquerySource(Box::new(SubqueryTableSource {
1584                        token: Token::new(TokenType::Punctuator, "(", Position::new(0, 0, 0)),
1585                        subquery: cte.query.clone(),
1586                        alias: Some(alias),
1587                    }))
1588                })
1589            }
1590            Expression::TableSource(ts) => {
1591                // Use pre-computed lowercase from value_lower
1592                let name = &ts.name.value_lower;
1593                cte_defs.get(name.as_str()).map(|cte| {
1594                    // Convert to SubquerySource preserving alias
1595                    let alias = ts.alias.clone().unwrap_or_else(|| ts.name.clone());
1596                    Expression::SubquerySource(Box::new(SubqueryTableSource {
1597                        token: Token::new(TokenType::Punctuator, "(", Position::new(0, 0, 0)),
1598                        subquery: cte.query.clone(),
1599                        alias: Some(alias),
1600                    }))
1601                })
1602            }
1603            Expression::JoinSource(js) => {
1604                let left_changed = self.try_inline_cte_references(&js.left, cte_defs);
1605                let right_changed = self.try_inline_cte_references(&js.right, cte_defs);
1606
1607                // Only create new JoinSource if something changed
1608                if left_changed.is_some() || right_changed.is_some() {
1609                    let left = left_changed.unwrap_or_else(|| (*js.left).clone());
1610                    let right = right_changed.unwrap_or_else(|| (*js.right).clone());
1611                    Some(Expression::JoinSource(Box::new(JoinTableSource {
1612                        token: js.token.clone(),
1613                        left: Box::new(left),
1614                        right: Box::new(right),
1615                        join_type: js.join_type.clone(),
1616                        condition: js.condition.clone(),
1617                        using_columns: js.using_columns.clone(),
1618                    })))
1619                } else {
1620                    None
1621                }
1622            }
1623            Expression::SubquerySource(sq) => {
1624                // Only recurse if there's a table_expr
1625                if let Some(ref table_expr) = sq.subquery.table_expr {
1626                    if let Some(inlined) = self.try_inline_cte_references(table_expr, cte_defs) {
1627                        let mut new_subquery = (*sq.subquery).clone();
1628                        new_subquery.table_expr = Some(Box::new(inlined));
1629                        return Some(Expression::SubquerySource(Box::new(SubqueryTableSource {
1630                            token: sq.token.clone(),
1631                            subquery: Box::new(new_subquery),
1632                            alias: sq.alias.clone(),
1633                        })));
1634                    }
1635                }
1636                None
1637            }
1638            _ => None, // No change needed
1639        }
1640    }
1641}