Skip to main content

sql_cli/query_plan/
cte_hoister.rs

1use crate::sql::parser::ast::{
2    CTEType, SelectItem, SelectStatement, SqlExpression, WhereClause, CTE,
3};
4use crate::sql::parser::walk;
5use std::collections::{HashMap, HashSet};
6
7/// CTE Hoister - Analyzes and rewrites nested CTEs
8///
9/// This module implements automatic CTE hoisting to transform nested WITH clauses
10/// into a flat list of CTEs at the query's top level. This enables natural nested
11/// query writing while maintaining compatibility with SQL execution.
12///
13/// Example transformation:
14/// ```sql
15/// -- Input (nested):
16/// SELECT * FROM (
17///   WITH inner_cte AS (SELECT ...)
18///   SELECT * FROM inner_cte
19/// )
20///
21/// -- Output (hoisted):
22/// WITH inner_cte AS (SELECT ...)
23/// SELECT * FROM inner_cte
24/// ```
25pub struct CTEHoister {
26    hoisted_ctes: Vec<CTE>,
27    _cte_counter: usize,
28    dependency_graph: HashMap<String, HashSet<String>>,
29}
30
31impl CTEHoister {
32    pub fn new() -> Self {
33        Self {
34            hoisted_ctes: Vec::new(),
35            _cte_counter: 0,
36            dependency_graph: HashMap::new(),
37        }
38    }
39
40    /// Hoist all nested CTEs to the top level
41    pub fn hoist_ctes(mut statement: SelectStatement) -> SelectStatement {
42        let mut hoister = CTEHoister::new();
43
44        // First collect any existing top-level CTEs
45        for cte in statement.ctes.drain(..) {
46            hoister.add_cte(cte);
47        }
48
49        // Then recursively hoist from the main statement
50        let rewritten = hoister.hoist_from_statement(statement);
51
52        // Build final statement with all hoisted CTEs
53        SelectStatement {
54            ctes: hoister.get_ordered_ctes(),
55            ..rewritten
56        }
57    }
58
59    /// Recursively hoist CTEs from a SELECT statement
60    fn hoist_from_statement(&mut self, mut statement: SelectStatement) -> SelectStatement {
61        // Hoist from subquery in FROM clause
62        statement.map_from_subquery(|subquery| {
63            let rewritten_sub = self.hoist_from_statement(subquery);
64
65            // If the subquery has CTEs, hoist them
66            for cte in rewritten_sub.ctes.clone() {
67                self.add_cte(cte);
68            }
69
70            // Return the subquery without its CTEs (they're hoisted)
71            SelectStatement {
72                ctes: Vec::new(),
73                ..rewritten_sub
74            }
75        });
76
77        // Hoist from CTEs in this statement
78        let local_ctes = statement.ctes.drain(..).collect::<Vec<_>>();
79        for mut cte in local_ctes {
80            // First hoist from within this CTE's query if it's a standard CTE
81            if let CTEType::Standard(query) = cte.cte_type {
82                let hoisted_query = self.hoist_from_statement(query);
83                cte.cte_type = CTEType::Standard(hoisted_query);
84            }
85            // Then add the CTE itself
86            self.add_cte(cte);
87        }
88
89        // Hoist from expressions in SELECT items
90        statement.select_items = statement
91            .select_items
92            .into_iter()
93            .map(|item| self.hoist_from_select_item(item))
94            .collect();
95
96        // Hoist from WHERE clause subqueries
97        if let Some(where_clause) = &mut statement.where_clause {
98            self.hoist_from_where_clause(where_clause);
99        }
100
101        // Return the statement without CTEs (they're all hoisted)
102        SelectStatement {
103            ctes: Vec::new(),
104            ..statement
105        }
106    }
107
108    /// Hoist CTEs from a SELECT item (for subqueries in expressions)
109    fn hoist_from_select_item(&mut self, item: SelectItem) -> SelectItem {
110        match item {
111            SelectItem::Expression {
112                expr,
113                alias,
114                leading_comments,
115                trailing_comment,
116            } => SelectItem::Expression {
117                expr: self.hoist_from_expression(expr),
118                alias,
119                leading_comments,
120                trailing_comment,
121            },
122            other => other,
123        }
124    }
125
126    /// Hoist CTEs from an expression
127    ///
128    /// The only real rule is the nested statements: recurse into them so their
129    /// CTEs get pulled up to the top level. `map_children` treats a subquery
130    /// statement as a scope boundary, and crossing it is precisely this
131    /// transformer's job, so this uses the `crossing` form -- which keeps the
132    /// list of subquery-bearing variants in `walk` rather than here.
133    ///
134    /// `self` is threaded through as the walk context because both closures
135    /// need it mutably; capturing it twice would not borrow-check.
136    fn hoist_from_expression(&mut self, expr: SqlExpression) -> SqlExpression {
137        walk::map_children_crossing(
138            expr,
139            self,
140            |h, e| h.hoist_from_expression(e),
141            |h, stmt| Box::new(h.hoist_from_statement(*stmt)),
142        )
143    }
144
145    /// Recursively hoist from a WHERE clause
146    fn hoist_from_where_clause(&mut self, where_clause: &mut WhereClause) {
147        for condition in &mut where_clause.conditions {
148            condition.expr = self.hoist_from_expression(condition.expr.clone());
149        }
150    }
151
152    /// Add a CTE to the hoisted collection
153    fn add_cte(&mut self, cte: CTE) {
154        // Track dependencies for proper ordering
155        self.analyze_cte_dependencies(&cte);
156        self.hoisted_ctes.push(cte);
157    }
158
159    /// Analyze CTE dependencies for proper ordering
160    fn analyze_cte_dependencies(&mut self, cte: &CTE) {
161        let mut deps = HashSet::new();
162        if let CTEType::Standard(query) = &cte.cte_type {
163            self.find_cte_references(query, &mut deps);
164        }
165        self.dependency_graph.insert(cte.name.clone(), deps);
166    }
167
168    /// Find all CTE references in a statement
169    fn find_cte_references(&self, statement: &SelectStatement, deps: &mut HashSet<String>) {
170        // Check if FROM references a CTE
171        if let Some(table) = &statement.from_table {
172            // Check if this table name is a CTE
173            for cte in &self.hoisted_ctes {
174                if cte.name == *table {
175                    deps.insert(table.clone());
176                }
177            }
178        }
179
180        // Check subquery references
181        if let Some(subquery) = &statement.from_subquery {
182            self.find_cte_references(subquery, deps);
183        }
184
185        // Check JOIN references
186        for join in &statement.joins {
187            // Check if join table is a CTE
188            if let crate::sql::parser::ast::TableSource::Table(table_name) = &join.table {
189                for cte in &self.hoisted_ctes {
190                    if cte.name == *table_name {
191                        deps.insert(table_name.clone());
192                    }
193                }
194            }
195        }
196
197        // Check expressions for CTE references
198        for item in &statement.select_items {
199            if let SelectItem::Expression { expr, .. } = item {
200                self.find_cte_refs_in_expression(expr, deps);
201            }
202        }
203
204        // Check WHERE clause
205        if let Some(where_clause) = &statement.where_clause {
206            for condition in &where_clause.conditions {
207                self.find_cte_refs_in_expression(&condition.expr, deps);
208            }
209        }
210    }
211
212    /// Find CTE references in an expression
213    ///
214    /// The only real rule is the nested statements: descend into them and look
215    /// for CTE references there. `visit_children` treats a subquery statement
216    /// as a scope boundary and will not enter it, so this uses the `crossing`
217    /// form. `deps` is the walk context -- both closures need it mutably.
218    fn find_cte_refs_in_expression(&self, expr: &SqlExpression, deps: &mut HashSet<String>) {
219        walk::visit_children_crossing(
220            expr,
221            deps,
222            |deps, child| self.find_cte_refs_in_expression(child, deps),
223            |deps, stmt| self.find_cte_references(stmt, deps),
224        )
225    }
226
227    /// Get CTEs in dependency order
228    fn get_ordered_ctes(self) -> Vec<CTE> {
229        // Simple topological sort
230        let mut result = Vec::new();
231        let mut visited = HashSet::new();
232        let mut temp_mark = HashSet::new();
233
234        fn visit(
235            name: &str,
236            graph: &HashMap<String, HashSet<String>>,
237            ctes: &[CTE],
238            visited: &mut HashSet<String>,
239            temp_mark: &mut HashSet<String>,
240            result: &mut Vec<CTE>,
241        ) {
242            if visited.contains(name) {
243                return;
244            }
245            if temp_mark.contains(name) {
246                // Circular dependency - for now just continue
247                return;
248            }
249
250            temp_mark.insert(name.to_string());
251
252            if let Some(deps) = graph.get(name) {
253                for dep in deps {
254                    visit(dep, graph, ctes, visited, temp_mark, result);
255                }
256            }
257
258            temp_mark.remove(name);
259            visited.insert(name.to_string());
260
261            // Find and add the CTE
262            if let Some(cte) = ctes.iter().find(|c| c.name == name) {
263                result.push(cte.clone());
264            }
265        }
266
267        // Visit all CTEs
268        for cte in &self.hoisted_ctes {
269            visit(
270                &cte.name,
271                &self.dependency_graph,
272                &self.hoisted_ctes,
273                &mut visited,
274                &mut temp_mark,
275                &mut result,
276            );
277        }
278
279        result
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    /// Regression: hoisting a derived table must rewrite the `from_source`
288    /// copy, not only the legacy `from_subquery`.
289    ///
290    /// The parser fills both with clones of the same subquery, and the executor
291    /// reads `from_source`. Rewriting one alone left a stale pre-hoist copy in
292    /// the field that actually gets executed.
293    ///
294    /// This currently produces the right answer either way — the stale copy is
295    /// a self-contained statement that still carries its own CTEs — so the
296    /// desync is latent, not a live wrong-results bug. The assertion pins the
297    /// two representations together before the correlated-subquery work starts
298    /// depending on `from_source` being authoritative.
299    #[test]
300    fn test_derived_table_hoisting_updates_from_source() {
301        use crate::sql::parser::ast::TableSource;
302        use crate::sql::recursive_parser::Parser;
303
304        let mut parser = Parser::new(
305            "SELECT symbol FROM (WITH x AS (SELECT symbol FROM trades) SELECT symbol FROM x) sub",
306        );
307        let stmt = parser.parse().expect("query should parse");
308
309        let hoisted = CTEHoister::hoist_ctes(stmt);
310
311        // The inner CTE was lifted to the top level.
312        assert_eq!(hoisted.ctes.len(), 1, "inner CTE should be hoisted");
313        assert_eq!(hoisted.ctes[0].name, "x");
314
315        // Both representations must show the CTE-stripped subquery. Before the
316        // fix, from_source still held the original with `ctes.len() == 1`.
317        #[allow(deprecated)]
318        let legacy = hoisted
319            .from_subquery
320            .as_ref()
321            .expect("from_subquery should be present");
322        assert!(legacy.ctes.is_empty(), "legacy copy should be stripped");
323
324        match hoisted.from_source {
325            Some(TableSource::DerivedTable {
326                ref query,
327                ref alias,
328            }) => {
329                assert!(
330                    query.ctes.is_empty(),
331                    "from_source holds a stale pre-hoist subquery with {} CTE(s)",
332                    query.ctes.len()
333                );
334                assert_eq!(alias, "sub", "derived-table alias must survive the rewrite");
335            }
336            ref other => panic!("expected a DerivedTable from_source, got {other:?}"),
337        }
338    }
339
340    #[test]
341    fn test_simple_cte_hoisting() {
342        // Test that a simple nested CTE gets hoisted
343        let inner_query = SelectStatement {
344            distinct: false,
345            columns: vec!["col1".to_string()],
346            select_items: vec![],
347            from_source: None,
348            #[allow(deprecated)]
349            from_table: Some("table1".to_string()),
350            #[allow(deprecated)]
351            from_subquery: None,
352            #[allow(deprecated)]
353            from_function: None,
354            #[allow(deprecated)]
355            from_alias: None,
356            joins: vec![],
357            where_clause: None,
358            order_by: None,
359            group_by: None,
360            having: None,
361            qualify: None,
362            limit: None,
363            offset: None,
364            ctes: vec![],
365            into_table: None,
366            set_operations: vec![],
367            leading_comments: vec![],
368            trailing_comment: None,
369        };
370
371        let nested_query = SelectStatement {
372            distinct: false,
373            columns: vec![],
374            select_items: vec![],
375            from_source: None,
376            #[allow(deprecated)]
377            from_subquery: Some(Box::new(SelectStatement {
378                distinct: false,
379                columns: vec![],
380                select_items: vec![],
381                ctes: vec![CTE {
382                    name: "inner".to_string(),
383                    column_list: None,
384                    cte_type: CTEType::Standard(inner_query),
385                }],
386                from_source: None,
387                #[allow(deprecated)]
388                from_table: Some("inner".to_string()),
389                #[allow(deprecated)]
390                from_subquery: None,
391                #[allow(deprecated)]
392                from_function: None,
393                #[allow(deprecated)]
394                from_alias: None,
395                joins: vec![],
396                where_clause: None,
397                order_by: None,
398                group_by: None,
399                having: None,
400                qualify: None,
401                limit: None,
402                offset: None,
403                into_table: None,
404                set_operations: vec![],
405                leading_comments: vec![],
406                trailing_comment: None,
407            })),
408            #[allow(deprecated)]
409            from_table: None,
410            #[allow(deprecated)]
411            from_function: None,
412            #[allow(deprecated)]
413            from_alias: None,
414            joins: vec![],
415            where_clause: None,
416            order_by: None,
417            group_by: None,
418            having: None,
419            qualify: None,
420            limit: None,
421            offset: None,
422            ctes: vec![],
423            into_table: None,
424            set_operations: vec![],
425            leading_comments: vec![],
426            trailing_comment: None,
427        };
428
429        let result = CTEHoister::hoist_ctes(nested_query);
430
431        assert_eq!(result.ctes.len(), 1);
432        assert_eq!(result.ctes[0].name, "inner");
433        assert!(result.from_subquery.as_ref().unwrap().ctes.is_empty());
434    }
435}