Skip to main content

sql_cli/query_plan/
where_alias_expander.rs

1//! WHERE clause alias expansion transformer
2//!
3//! This transformer allows users to reference SELECT clause aliases in WHERE clauses
4//! by automatically expanding those aliases to their full expressions.
5//!
6//! # Problem
7//!
8//! Users often want to reference complex SELECT expressions by their aliases in WHERE:
9//! ```sql
10//! SELECT a, a * 2 as double_a FROM t WHERE double_a > 10
11//! ```
12//!
13//! This fails because WHERE is evaluated before SELECT, so aliases don't exist yet.
14//!
15//! # Solution
16//!
17//! The transformer rewrites to:
18//! ```sql
19//! SELECT a, a * 2 as double_a FROM t WHERE a * 2 > 10
20//! ```
21//!
22//! # Algorithm
23//!
24//! 1. Extract all aliases from SELECT clause and their corresponding expressions
25//! 2. Scan WHERE clause for column references
26//! 3. If a column reference matches an alias name, replace it with the full expression
27//! 4. Handle nested expressions (BinaryOp, CASE, etc.) recursively
28//!
29//! # Limitations
30//!
31//! - Only works for simple column aliases (not table.alias references)
32//! - Aliases take precedence over actual column names if they conflict
33//! - Complex expressions are duplicated (no common subexpression elimination)
34
35use crate::query_plan::pipeline::ASTTransformer;
36use crate::sql::parser::ast::{SelectItem, SelectStatement, SqlExpression};
37use crate::sql::parser::walk;
38use anyhow::Result;
39use std::collections::HashMap;
40use tracing::debug;
41
42/// Transformer that expands SELECT aliases in WHERE clauses
43pub struct WhereAliasExpander {
44    /// Counter for tracking number of expansions
45    expansions: usize,
46}
47
48impl WhereAliasExpander {
49    pub fn new() -> Self {
50        Self { expansions: 0 }
51    }
52
53    /// Extract aliases from SELECT clause
54    /// Returns a map of alias name -> expression
55    fn extract_aliases(select_items: &[SelectItem]) -> HashMap<String, SqlExpression> {
56        let mut aliases = HashMap::new();
57
58        for item in select_items {
59            if let SelectItem::Expression { expr, alias, .. } = item {
60                if !alias.is_empty() {
61                    aliases.insert(alias.clone(), expr.clone());
62                    debug!("Found SELECT alias: {} -> {:?}", alias, expr);
63                }
64            }
65        }
66
67        aliases
68    }
69
70    /// Recursively expand aliases in an expression.
71    /// Returns the expanded expression and whether any expansion occurred.
72    ///
73    /// Only two node kinds carry a real rule; everything else is pure structural
74    /// recursion, so it is delegated to [`walk::map_children`]. That helper is
75    /// exhaustive by construction and treats a nested subquery's `SelectStatement`
76    /// as an **opaque scope boundary** — it descends into the same-scope operands
77    /// of `InSubquery` / `NotInSubquery` / the tuple forms (fixing P11: an alias
78    /// on the LHS of `x IN (SELECT ...)`) while never reaching into the subquery
79    /// body, where an outer alias must not leak. Before this migration those
80    /// variants fell into a `_ => (clone, false)` catch-all and the LHS operand
81    /// was silently skipped along with the subquery.
82    fn expand_expression(
83        expr: &SqlExpression,
84        aliases: &HashMap<String, SqlExpression>,
85    ) -> (SqlExpression, bool) {
86        match expr {
87            // Rule 1: a bare (un-prefixed) column reference that names a SELECT
88            // alias is replaced by that alias's expression.
89            SqlExpression::Column(col_ref) => {
90                if col_ref.table_prefix.is_none() {
91                    if let Some(alias_expr) = aliases.get(&col_ref.name) {
92                        debug!(
93                            "Expanding alias '{}' in WHERE to: {:?}",
94                            col_ref.name, alias_expr
95                        );
96                        return (alias_expr.clone(), true);
97                    }
98                }
99                (expr.clone(), false)
100            }
101
102            // Rule 2: a method call's receiver is a bare column-name *string*, not
103            // a child expression, so the walker can't reach it. Substitute the
104            // receiver here when it names an alias resolving to a simple column,
105            // then recurse into the args normally.
106            SqlExpression::MethodCall {
107                object,
108                method,
109                args,
110            } => {
111                let mut expanded = false;
112                let new_args: Vec<SqlExpression> = args
113                    .iter()
114                    .map(|arg| {
115                        let (new_arg, arg_expanded) = Self::expand_expression(arg, aliases);
116                        expanded = expanded || arg_expanded;
117                        new_arg
118                    })
119                    .collect();
120
121                let mut new_object = object.clone();
122                if let Some(SqlExpression::Column(col_ref)) = aliases.get(object) {
123                    if col_ref.table_prefix.is_none() {
124                        debug!(
125                            "Expanding alias '{}' in WHERE method call to column '{}'",
126                            object, col_ref.name
127                        );
128                        new_object = col_ref.name.clone();
129                        expanded = true;
130                    }
131                }
132
133                (
134                    SqlExpression::MethodCall {
135                        object: new_object,
136                        method: method.clone(),
137                        args: new_args,
138                    },
139                    expanded,
140                )
141            }
142
143            // Everything else: structural recursion via the walker. It visits
144            // same-scope children (including subquery LHS operands) and leaves
145            // subquery bodies opaque.
146            other => {
147                let mut expanded = false;
148                let new_expr = walk::map_children(other.clone(), |child| {
149                    let (new_child, child_expanded) = Self::expand_expression(&child, aliases);
150                    expanded = expanded || child_expanded;
151                    new_child
152                });
153                (new_expr, expanded)
154            }
155        }
156    }
157
158    /// Expand aliases in WHERE clause conditions
159    fn expand_where_clause(
160        &mut self,
161        where_clause: &mut crate::sql::parser::ast::WhereClause,
162        aliases: &HashMap<String, SqlExpression>,
163    ) -> bool {
164        let mut any_expanded = false;
165
166        for condition in &mut where_clause.conditions {
167            let (new_expr, expanded) = Self::expand_expression(&condition.expr, aliases);
168            if expanded {
169                condition.expr = new_expr;
170                any_expanded = true;
171                self.expansions += 1;
172            }
173        }
174
175        any_expanded
176    }
177}
178
179impl Default for WhereAliasExpander {
180    fn default() -> Self {
181        Self::new()
182    }
183}
184
185impl ASTTransformer for WhereAliasExpander {
186    fn name(&self) -> &str {
187        "WhereAliasExpander"
188    }
189
190    fn description(&self) -> &str {
191        "Expands SELECT aliases in WHERE clauses to their full expressions"
192    }
193
194    fn transform(&mut self, mut stmt: SelectStatement) -> Result<SelectStatement> {
195        // Only process if there's a WHERE clause
196        if stmt.where_clause.is_none() {
197            return Ok(stmt);
198        }
199
200        // Step 1: Extract all aliases from SELECT clause
201        let aliases = Self::extract_aliases(&stmt.select_items);
202
203        if aliases.is_empty() {
204            // No aliases to expand
205            return Ok(stmt);
206        }
207
208        // Step 2: Expand aliases in WHERE clause
209        if let Some(ref mut where_clause) = stmt.where_clause {
210            let expanded = self.expand_where_clause(where_clause, &aliases);
211            if expanded {
212                debug!(
213                    "Expanded {} alias reference(s) in WHERE clause",
214                    self.expansions
215                );
216            }
217        }
218
219        Ok(stmt)
220    }
221
222    fn begin(&mut self) -> Result<()> {
223        // Reset expansion counter for each query
224        self.expansions = 0;
225        Ok(())
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use crate::sql::parser::ast::{ColumnRef, Condition, QuoteStyle, WhereClause};
233
234    #[test]
235    fn test_extract_aliases() {
236        let double_a_expr = SqlExpression::BinaryOp {
237            left: Box::new(SqlExpression::Column(ColumnRef {
238                name: "a".to_string(),
239                quote_style: QuoteStyle::None,
240                table_prefix: None,
241            })),
242            op: "*".to_string(),
243            right: Box::new(SqlExpression::NumberLiteral("2".to_string())),
244        };
245
246        let select_items = vec![SelectItem::Expression {
247            expr: double_a_expr.clone(),
248            alias: "double_a".to_string(),
249            leading_comments: vec![],
250            trailing_comment: None,
251        }];
252
253        let aliases = WhereAliasExpander::extract_aliases(&select_items);
254        assert_eq!(aliases.len(), 1);
255        assert!(aliases.contains_key("double_a"));
256    }
257
258    #[test]
259    fn test_expand_simple_column_reference() {
260        let aliases = HashMap::from([(
261            "double_a".to_string(),
262            SqlExpression::BinaryOp {
263                left: Box::new(SqlExpression::Column(ColumnRef::unquoted("a".to_string()))),
264                op: "*".to_string(),
265                right: Box::new(SqlExpression::NumberLiteral("2".to_string())),
266            },
267        )]);
268
269        let expr = SqlExpression::Column(ColumnRef::unquoted("double_a".to_string()));
270        let (expanded, changed) = WhereAliasExpander::expand_expression(&expr, &aliases);
271
272        assert!(changed);
273        assert!(matches!(expanded, SqlExpression::BinaryOp { .. }));
274    }
275
276    #[test]
277    fn test_expand_in_binary_op() {
278        let aliases = HashMap::from([(
279            "double_a".to_string(),
280            SqlExpression::BinaryOp {
281                left: Box::new(SqlExpression::Column(ColumnRef::unquoted("a".to_string()))),
282                op: "*".to_string(),
283                right: Box::new(SqlExpression::NumberLiteral("2".to_string())),
284            },
285        )]);
286
287        let expr = SqlExpression::BinaryOp {
288            left: Box::new(SqlExpression::Column(ColumnRef::unquoted(
289                "double_a".to_string(),
290            ))),
291            op: ">".to_string(),
292            right: Box::new(SqlExpression::NumberLiteral("10".to_string())),
293        };
294
295        let (expanded, changed) = WhereAliasExpander::expand_expression(&expr, &aliases);
296
297        assert!(changed);
298        if let SqlExpression::BinaryOp { left, op, right } = expanded {
299            assert_eq!(op, ">");
300            assert!(matches!(left.as_ref(), SqlExpression::BinaryOp { .. }));
301            assert!(matches!(
302                right.as_ref(),
303                SqlExpression::NumberLiteral(s) if s == "10"
304            ));
305        } else {
306            panic!("Expected BinaryOp");
307        }
308    }
309
310    #[test]
311    fn test_transform_with_no_where() {
312        let mut transformer = WhereAliasExpander::new();
313        let stmt = SelectStatement {
314            where_clause: None,
315            ..Default::default()
316        };
317
318        let result = transformer.transform(stmt);
319        assert!(result.is_ok());
320    }
321
322    #[test]
323    fn test_transform_expands_alias() {
324        let mut transformer = WhereAliasExpander::new();
325
326        let double_a_expr = SqlExpression::BinaryOp {
327            left: Box::new(SqlExpression::Column(ColumnRef::unquoted("a".to_string()))),
328            op: "*".to_string(),
329            right: Box::new(SqlExpression::NumberLiteral("2".to_string())),
330        };
331
332        let stmt = SelectStatement {
333            select_items: vec![SelectItem::Expression {
334                expr: double_a_expr.clone(),
335                alias: "double_a".to_string(),
336                leading_comments: vec![],
337                trailing_comment: None,
338            }],
339            where_clause: Some(WhereClause {
340                conditions: vec![Condition {
341                    expr: SqlExpression::BinaryOp {
342                        left: Box::new(SqlExpression::Column(ColumnRef::unquoted(
343                            "double_a".to_string(),
344                        ))),
345                        op: ">".to_string(),
346                        right: Box::new(SqlExpression::NumberLiteral("10".to_string())),
347                    },
348                    connector: None,
349                }],
350            }),
351            ..Default::default()
352        };
353
354        let result = transformer.transform(stmt).unwrap();
355
356        // Check that WHERE was rewritten
357        if let Some(where_clause) = &result.where_clause {
358            if let SqlExpression::BinaryOp { left, .. } = &where_clause.conditions[0].expr {
359                // Left side should now be the expanded expression (a * 2), not the column "double_a"
360                assert!(matches!(left.as_ref(), SqlExpression::BinaryOp { .. }));
361            } else {
362                panic!("Expected BinaryOp in WHERE");
363            }
364        } else {
365            panic!("Expected WHERE clause");
366        }
367
368        assert_eq!(transformer.expansions, 1);
369    }
370
371    #[test]
372    fn test_expand_alias_in_method_call_receiver() {
373        // `SELECT "name.common" as name ... WHERE name.Contains('x')`
374        // The alias `name` resolves to the column `name.common`, so the method
375        // call's receiver should be rewritten to that column name.
376        let aliases = HashMap::from([(
377            "name".to_string(),
378            SqlExpression::Column(ColumnRef {
379                name: "name.common".to_string(),
380                quote_style: QuoteStyle::DoubleQuotes,
381                table_prefix: None,
382            }),
383        )]);
384
385        let expr = SqlExpression::MethodCall {
386            object: "name".to_string(),
387            method: "Contains".to_string(),
388            args: vec![SqlExpression::StringLiteral("united".to_string())],
389        };
390
391        let (expanded, changed) = WhereAliasExpander::expand_expression(&expr, &aliases);
392
393        assert!(changed);
394        match expanded {
395            SqlExpression::MethodCall { object, method, .. } => {
396                assert_eq!(object, "name.common");
397                assert_eq!(method, "Contains");
398            }
399            other => panic!("Expected MethodCall, got {other:?}"),
400        }
401    }
402
403    #[test]
404    fn test_does_not_expand_method_call_for_nonalias() {
405        // A method call whose receiver is a real column (not an alias) is untouched.
406        let aliases = HashMap::from([(
407            "name".to_string(),
408            SqlExpression::Column(ColumnRef::unquoted("name.common".to_string())),
409        )]);
410
411        let expr = SqlExpression::MethodCall {
412            object: "capital".to_string(),
413            method: "Contains".to_string(),
414            args: vec![SqlExpression::StringLiteral("x".to_string())],
415        };
416
417        let (expanded, changed) = WhereAliasExpander::expand_expression(&expr, &aliases);
418
419        assert!(!changed);
420        assert!(matches!(
421            expanded,
422            SqlExpression::MethodCall { object, .. } if object == "capital"
423        ));
424    }
425
426    #[test]
427    fn test_expands_alias_on_in_subquery_lhs_not_body() {
428        // P11: `WHERE dbl IN (SELECT ...)` where `dbl` aliases `price * 2`.
429        // The walker migration must expand the same-scope LHS operand while
430        // leaving the subquery body (a different scope) untouched.
431        let double = SqlExpression::BinaryOp {
432            left: Box::new(SqlExpression::Column(ColumnRef::unquoted("price".into()))),
433            op: "*".to_string(),
434            right: Box::new(SqlExpression::NumberLiteral("2".to_string())),
435        };
436        let aliases = HashMap::from([("dbl".to_string(), double.clone())]);
437
438        // The subquery body also references `dbl` — it must NOT be expanded,
439        // because that name belongs to the subquery's own scope.
440        let body = SelectStatement {
441            where_clause: Some(WhereClause {
442                conditions: vec![Condition {
443                    expr: SqlExpression::Column(ColumnRef::unquoted("dbl".into())),
444                    connector: None,
445                }],
446            }),
447            ..Default::default()
448        };
449
450        let expr = SqlExpression::InSubquery {
451            expr: Box::new(SqlExpression::Column(ColumnRef::unquoted("dbl".into()))),
452            subquery: Box::new(body.clone()),
453        };
454
455        let (expanded, changed) = WhereAliasExpander::expand_expression(&expr, &aliases);
456        assert!(changed, "the LHS alias should have been expanded");
457
458        match expanded {
459            SqlExpression::InSubquery { expr, subquery } => {
460                // LHS expanded to the aliased expression.
461                assert!(matches!(expr.as_ref(), SqlExpression::BinaryOp { .. }));
462                // Subquery body left verbatim: still the bare `dbl` column.
463                let inner = &subquery.where_clause.as_ref().unwrap().conditions[0].expr;
464                assert!(
465                    matches!(inner, SqlExpression::Column(c) if c.name == "dbl"),
466                    "subquery body must not be touched (different scope), got {inner:?}"
467                );
468            }
469            other => panic!("expected InSubquery, got {other:?}"),
470        }
471    }
472
473    #[test]
474    fn test_does_not_expand_table_prefixed_columns() {
475        let aliases = HashMap::from([(
476            "double_a".to_string(),
477            SqlExpression::BinaryOp {
478                left: Box::new(SqlExpression::Column(ColumnRef::unquoted("a".to_string()))),
479                op: "*".to_string(),
480                right: Box::new(SqlExpression::NumberLiteral("2".to_string())),
481            },
482        )]);
483
484        // Column with table prefix should NOT be expanded
485        let expr = SqlExpression::Column(ColumnRef {
486            name: "double_a".to_string(),
487            quote_style: QuoteStyle::None,
488            table_prefix: Some("t".to_string()),
489        });
490
491        let (expanded, changed) = WhereAliasExpander::expand_expression(&expr, &aliases);
492
493        assert!(!changed);
494        assert!(matches!(expanded, SqlExpression::Column(_)));
495    }
496}