Skip to main content

sql_cli/query_plan/
into_clause_remover.rs

1use crate::sql::parser::ast::{SelectStatement, SqlExpression};
2use crate::sql::parser::walk;
3
4/// INTO Clause Remover - Removes INTO clause from AST for execution
5///
6/// This module implements AST rewriting to remove the INTO clause from
7/// SELECT statements. The INTO clause is used to store query results in
8/// temporary tables, but the query executor doesn't understand this syntax.
9///
10/// The removal is done at the AST level (not via regex) to ensure correctness
11/// and maintainability. The caller is responsible for capturing the INTO table
12/// information before removal and storing the results after execution.
13///
14/// Example transformation:
15/// ```sql
16/// -- Input:
17/// SELECT col1, col2 INTO #temp FROM table WHERE x > 5
18///
19/// -- Output (for execution):
20/// SELECT col1, col2 FROM table WHERE x > 5
21/// ```
22pub struct IntoClauseRemover;
23
24impl IntoClauseRemover {
25    /// Remove INTO clause from statement and all nested subqueries
26    ///
27    /// This creates a new statement with `into_table` set to None.
28    /// The original statement is not modified.
29    ///
30    /// # Arguments
31    /// * `statement` - The SELECT statement to process
32    ///
33    /// # Returns
34    /// A new statement with INTO clause removed from all levels
35    pub fn remove_into_clause(statement: SelectStatement) -> SelectStatement {
36        Self::remove_from_statement(statement)
37    }
38
39    /// Recursively remove INTO clause from a statement and its subqueries
40    fn remove_from_statement(mut statement: SelectStatement) -> SelectStatement {
41        // Remove the INTO clause from this statement
42        statement.into_table = None;
43
44        // Remove from subquery in FROM clause
45        statement.map_from_subquery(Self::remove_from_statement);
46
47        // Remove from JOIN subqueries
48        statement.joins = statement
49            .joins
50            .into_iter()
51            .map(|mut join| {
52                if let crate::sql::parser::ast::TableSource::DerivedTable { query, alias } =
53                    join.table
54                {
55                    join.table = crate::sql::parser::ast::TableSource::DerivedTable {
56                        query: Box::new(Self::remove_from_statement(*query)),
57                        alias,
58                    };
59                }
60                join
61            })
62            .collect();
63
64        // Remove from scalar subqueries and other expression subqueries
65        statement.select_items = statement
66            .select_items
67            .into_iter()
68            .map(|item| Self::remove_from_select_item(item))
69            .collect();
70
71        // Remove from WHERE clause subqueries
72        if let Some(mut where_clause) = statement.where_clause.take() {
73            for condition in &mut where_clause.conditions {
74                condition.expr = Self::remove_from_expression(condition.expr.clone());
75            }
76            statement.where_clause = Some(where_clause);
77        }
78
79        // Remove from set operation queries (UNION, INTERSECT, EXCEPT)
80        statement.set_operations = statement
81            .set_operations
82            .into_iter()
83            .map(|(op, query)| (op, Box::new(Self::remove_from_statement(*query))))
84            .collect();
85
86        statement
87    }
88
89    /// Remove INTO from SELECT items (handles subqueries in expressions)
90    fn remove_from_select_item(
91        item: crate::sql::parser::ast::SelectItem,
92    ) -> crate::sql::parser::ast::SelectItem {
93        match item {
94            crate::sql::parser::ast::SelectItem::Expression {
95                expr,
96                alias,
97                leading_comments,
98                trailing_comment,
99            } => crate::sql::parser::ast::SelectItem::Expression {
100                expr: Self::remove_from_expression(expr),
101                alias,
102                leading_comments,
103                trailing_comment,
104            },
105            other => other,
106        }
107    }
108
109    /// Remove INTO from expressions (handles subqueries)
110    ///
111    /// The only real rule here is about subqueries: every nested
112    /// `SelectStatement` needs its `into_table` cleared. Everything else is
113    /// plain traversal, delegated to [`walk::map_children_crossing`].
114    ///
115    /// `map_children` treats a subquery statement as a **scope boundary** and
116    /// deliberately does not descend into it — correct for the alias expanders,
117    /// but exactly what this transformer has to do, hence the `crossing` form.
118    fn remove_from_expression(expr: SqlExpression) -> SqlExpression {
119        walk::map_children_crossing(
120            expr,
121            &mut (),
122            |_, e| Self::remove_from_expression(e),
123            |_, stmt| Box::new(Self::remove_from_statement(*stmt)),
124        )
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::sql::parser::ast::IntoTable;
132
133    /// Regression for the walk.rs migration: the tuple subquery forms used to
134    /// fall into the hand-rolled catch-all, so an INTO inside
135    /// `(a, b) IN (SELECT ...)` was never removed and would reach the executor.
136    ///
137    /// Parsed rather than hand-built so the AST is one the parser actually
138    /// produces (see R4 in docs/ENGINE_REFACTORING.md).
139    #[test]
140    fn removes_into_inside_tuple_subquery() {
141        use crate::sql::recursive_parser::Parser;
142
143        let stmt = Parser::new("SELECT a FROM t WHERE (a, b) IN (SELECT x, y FROM u INTO #inner)")
144            .parse()
145            .expect("query should parse");
146
147        // Precondition: the parser really did put an INTO on the inner query.
148        let inner_into = |s: &SelectStatement| match &s.where_clause {
149            Some(w) => match &w.conditions[0].expr {
150                SqlExpression::InSubqueryTuple { subquery, .. } => subquery.into_table.clone(),
151                other => panic!("expected a tuple IN subquery, got {other:?}"),
152            },
153            None => panic!("expected a where clause"),
154        };
155        assert!(
156            inner_into(&stmt).is_some(),
157            "test is meaningless unless the inner query starts with an INTO"
158        );
159
160        let result = IntoClauseRemover::remove_into_clause(stmt);
161        assert!(
162            inner_into(&result).is_none(),
163            "INTO must be removed from inside a tuple subquery"
164        );
165    }
166
167    #[test]
168    fn test_remove_simple_into() {
169        let stmt = SelectStatement {
170            distinct: false,
171            columns: vec!["col1".to_string()],
172            select_items: vec![],
173            from_source: None,
174            #[allow(deprecated)]
175            from_table: Some("table1".to_string()),
176            #[allow(deprecated)]
177            from_subquery: None,
178            #[allow(deprecated)]
179            from_function: None,
180            #[allow(deprecated)]
181            from_alias: None,
182            joins: vec![],
183            where_clause: None,
184            order_by: None,
185            group_by: None,
186            having: None,
187            qualify: None,
188            limit: None,
189            offset: None,
190            ctes: vec![],
191            into_table: Some(IntoTable {
192                name: "#temp".to_string(),
193            }),
194            set_operations: vec![],
195            leading_comments: vec![],
196            trailing_comment: None,
197        };
198
199        let result = IntoClauseRemover::remove_into_clause(stmt);
200        assert!(result.into_table.is_none());
201        assert_eq!(result.from_table, Some("table1".to_string()));
202    }
203
204    #[test]
205    fn test_remove_into_from_subquery() {
206        let subquery = SelectStatement {
207            distinct: false,
208            columns: vec![],
209            select_items: vec![],
210            from_source: None,
211            #[allow(deprecated)]
212            from_table: Some("inner_table".to_string()),
213            #[allow(deprecated)]
214            from_subquery: None,
215            #[allow(deprecated)]
216            from_function: None,
217            #[allow(deprecated)]
218            from_alias: None,
219            joins: vec![],
220            where_clause: None,
221            order_by: None,
222            group_by: None,
223            having: None,
224            qualify: None,
225            limit: None,
226            offset: None,
227            ctes: vec![],
228            into_table: Some(IntoTable {
229                name: "#inner_temp".to_string(),
230            }),
231            set_operations: vec![],
232            leading_comments: vec![],
233            trailing_comment: None,
234        };
235
236        let stmt = SelectStatement {
237            distinct: false,
238            columns: vec![],
239            select_items: vec![],
240            from_source: None,
241            #[allow(deprecated)]
242            from_table: None,
243            #[allow(deprecated)]
244            from_subquery: Some(Box::new(subquery)),
245            #[allow(deprecated)]
246            from_function: None,
247            #[allow(deprecated)]
248            from_alias: Some("subq".to_string()),
249            joins: vec![],
250            where_clause: None,
251            order_by: None,
252            group_by: None,
253            having: None,
254            qualify: None,
255            limit: None,
256            offset: None,
257            ctes: vec![],
258            into_table: Some(IntoTable {
259                name: "#outer_temp".to_string(),
260            }),
261            set_operations: vec![],
262            leading_comments: vec![],
263            trailing_comment: None,
264        };
265
266        let result = IntoClauseRemover::remove_into_clause(stmt);
267
268        // Both outer and inner INTO should be removed
269        assert!(result.into_table.is_none());
270        assert!(result.from_subquery.as_ref().unwrap().into_table.is_none());
271    }
272}