Skip to main content

polyglot_sql/optimizer/
canonicalize.rs

1//! Canonicalization Module
2//!
3//! This module provides functionality for converting SQL expressions into a
4//! standard canonical form. This includes:
5//! - Converting string addition to CONCAT
6//! - Replacing date functions with casts
7//! - Removing redundant type casts
8//! - Ensuring boolean predicates
9//! - Removing unnecessary ASC from ORDER BY
10//!
11//! Ported from sqlglot's optimizer/canonicalize.py
12
13use crate::dialects::DialectType;
14use crate::expressions::{DataType, Expression, Literal, Null};
15use crate::helper::{is_iso_date, is_iso_datetime};
16
17/// Converts a SQL expression into a standard canonical form.
18///
19/// This transformation relies on type annotations because many of the
20/// conversions depend on type inference.
21///
22/// # Arguments
23/// * `expression` - The expression to canonicalize
24/// * `dialect` - Optional dialect for dialect-specific behavior
25///
26/// # Returns
27/// The canonicalized expression
28pub fn canonicalize(expression: Expression, dialect: Option<DialectType>) -> Expression {
29    canonicalize_recursive(expression, dialect)
30}
31
32/// Recursively canonicalize an expression and its children
33fn canonicalize_recursive(expression: Expression, dialect: Option<DialectType>) -> Expression {
34    let expr = match expression {
35        Expression::Select(mut select) => {
36            // Canonicalize SELECT expressions
37            select.expressions = select
38                .expressions
39                .into_iter()
40                .map(|e| canonicalize_recursive(e, dialect))
41                .collect();
42
43            // Canonicalize FROM
44            if let Some(mut from) = select.from {
45                from.expressions = from
46                    .expressions
47                    .into_iter()
48                    .map(|e| canonicalize_recursive(e, dialect))
49                    .collect();
50                select.from = Some(from);
51            }
52
53            // Canonicalize WHERE
54            if let Some(mut where_clause) = select.where_clause {
55                where_clause.this = canonicalize_recursive(where_clause.this, dialect);
56                where_clause.this = ensure_bools(where_clause.this);
57                select.where_clause = Some(where_clause);
58            }
59
60            // Canonicalize HAVING
61            if let Some(mut having) = select.having {
62                having.this = canonicalize_recursive(having.this, dialect);
63                having.this = ensure_bools(having.this);
64                select.having = Some(having);
65            }
66
67            // Canonicalize ORDER BY
68            if let Some(mut order_by) = select.order_by {
69                order_by.expressions = order_by
70                    .expressions
71                    .into_iter()
72                    .map(|mut o| {
73                        o.this = canonicalize_recursive(o.this, dialect);
74                        o = remove_ascending_order(o);
75                        o
76                    })
77                    .collect();
78                select.order_by = Some(order_by);
79            }
80
81            // Canonicalize JOINs
82            select.joins = select
83                .joins
84                .into_iter()
85                .map(|mut j| {
86                    j.this = canonicalize_recursive(j.this, dialect);
87                    if let Some(on) = j.on {
88                        j.on = Some(canonicalize_recursive(on, dialect));
89                    }
90                    j
91                })
92                .collect();
93
94            Expression::Select(select)
95        }
96
97        // Binary operations that might involve string addition
98        Expression::Add(bin) => {
99            let left = canonicalize_recursive(bin.left, dialect);
100            let right = canonicalize_recursive(bin.right, dialect);
101            let result = Expression::Add(Box::new(crate::expressions::BinaryOp {
102                left,
103                right,
104                left_comments: bin.left_comments,
105                operator_comments: bin.operator_comments,
106                trailing_comments: bin.trailing_comments,
107                inferred_type: None,
108            }));
109            add_text_to_concat(result)
110        }
111
112        // Other binary operations
113        Expression::And(bin) => {
114            let left = ensure_bools(canonicalize_recursive(bin.left, dialect));
115            let right = ensure_bools(canonicalize_recursive(bin.right, dialect));
116            Expression::And(Box::new(crate::expressions::BinaryOp {
117                left,
118                right,
119                left_comments: bin.left_comments,
120                operator_comments: bin.operator_comments,
121                trailing_comments: bin.trailing_comments,
122                inferred_type: None,
123            }))
124        }
125        Expression::Or(bin) => {
126            let left = ensure_bools(canonicalize_recursive(bin.left, dialect));
127            let right = ensure_bools(canonicalize_recursive(bin.right, dialect));
128            Expression::Or(Box::new(crate::expressions::BinaryOp {
129                left,
130                right,
131                left_comments: bin.left_comments,
132                operator_comments: bin.operator_comments,
133                trailing_comments: bin.trailing_comments,
134                inferred_type: None,
135            }))
136        }
137
138        Expression::Not(un) => {
139            let inner = ensure_bools(canonicalize_recursive(un.this, dialect));
140            Expression::Not(Box::new(crate::expressions::UnaryOp {
141                this: inner,
142                inferred_type: None,
143            }))
144        }
145
146        // Comparison operations - check for date coercion
147        Expression::Eq(bin) => canonicalize_comparison(Expression::Eq, *bin, dialect),
148        Expression::Neq(bin) => canonicalize_comparison(Expression::Neq, *bin, dialect),
149        Expression::Lt(bin) => canonicalize_comparison(Expression::Lt, *bin, dialect),
150        Expression::Lte(bin) => canonicalize_comparison(Expression::Lte, *bin, dialect),
151        Expression::Gt(bin) => canonicalize_comparison(Expression::Gt, *bin, dialect),
152        Expression::Gte(bin) => canonicalize_comparison(Expression::Gte, *bin, dialect),
153
154        Expression::Sub(bin) => canonicalize_comparison(Expression::Sub, *bin, dialect),
155        Expression::Mul(bin) => canonicalize_binary(Expression::Mul, *bin, dialect),
156        Expression::Div(bin) => canonicalize_binary(Expression::Div, *bin, dialect),
157
158        // Cast - check for redundancy
159        Expression::Cast(cast) => {
160            let inner = canonicalize_recursive(cast.this, dialect);
161            let result = Expression::Cast(Box::new(crate::expressions::Cast {
162                this: inner,
163                to: cast.to,
164                trailing_comments: cast.trailing_comments,
165                double_colon_syntax: cast.double_colon_syntax,
166                format: cast.format,
167                default: cast.default,
168                inferred_type: None,
169            }));
170            remove_redundant_casts(result)
171        }
172
173        // Preserve source metadata while canonicalizing shared function children.
174        Expression::Function(mut func) => {
175            func.args = func
176                .args
177                .into_iter()
178                .map(|e| canonicalize_recursive(e, dialect))
179                .collect();
180            if let Some(behavior) = &mut func.on_error {
181                behavior.value = behavior
182                    .value
183                    .take()
184                    .map(|e| canonicalize_recursive(e, dialect));
185            }
186            func.span = None;
187            func.inferred_type = None;
188            Expression::Function(func)
189        }
190        Expression::AggregateFunction(mut agg) => {
191            agg.args = agg
192                .args
193                .into_iter()
194                .map(|e| canonicalize_recursive(e, dialect))
195                .collect();
196            agg.filter = agg.filter.map(|e| canonicalize_recursive(e, dialect));
197            agg.inferred_type = None;
198            Expression::AggregateFunction(agg)
199        }
200
201        // Alias
202        Expression::Alias(alias) => {
203            let inner = canonicalize_recursive(alias.this, dialect);
204            Expression::Alias(Box::new(crate::expressions::Alias {
205                this: inner,
206                alias: alias.alias,
207                column_aliases: alias.column_aliases,
208                alias_explicit_as: false,
209                alias_keyword: None,
210                pre_alias_comments: alias.pre_alias_comments,
211                trailing_comments: alias.trailing_comments,
212                inferred_type: None,
213            }))
214        }
215
216        // Paren
217        Expression::Paren(paren) => {
218            let inner = canonicalize_recursive(paren.this, dialect);
219            Expression::Paren(Box::new(crate::expressions::Paren {
220                this: inner,
221                trailing_comments: paren.trailing_comments,
222            }))
223        }
224
225        // Case
226        Expression::Case(case) => {
227            let operand = case.operand.map(|e| canonicalize_recursive(e, dialect));
228            let whens = case
229                .whens
230                .into_iter()
231                .map(|(w, t)| {
232                    (
233                        canonicalize_recursive(w, dialect),
234                        canonicalize_recursive(t, dialect),
235                    )
236                })
237                .collect();
238            let else_ = case.else_.map(|e| canonicalize_recursive(e, dialect));
239            Expression::Case(Box::new(crate::expressions::Case {
240                operand,
241                whens,
242                else_,
243                comments: Vec::new(),
244                inferred_type: None,
245            }))
246        }
247
248        // Between - check for date coercion
249        Expression::Between(between) => {
250            let this = canonicalize_recursive(between.this, dialect);
251            let low = canonicalize_recursive(between.low, dialect);
252            let high = canonicalize_recursive(between.high, dialect);
253            Expression::Between(Box::new(crate::expressions::Between {
254                this,
255                low,
256                high,
257                not: between.not,
258                symmetric: between.symmetric,
259            }))
260        }
261
262        // In
263        Expression::In(in_expr) => {
264            let this = canonicalize_recursive(in_expr.this, dialect);
265            let expressions = in_expr
266                .expressions
267                .into_iter()
268                .map(|e| canonicalize_recursive(e, dialect))
269                .collect();
270            let query = in_expr.query.map(|q| canonicalize_recursive(q, dialect));
271            Expression::In(Box::new(crate::expressions::In {
272                this,
273                expressions,
274                query,
275                not: in_expr.not,
276                global: in_expr.global,
277                unnest: in_expr.unnest,
278                is_field: in_expr.is_field,
279            }))
280        }
281
282        // Subquery
283        Expression::Subquery(subquery) => {
284            let this = canonicalize_recursive(subquery.this, dialect);
285            Expression::Subquery(Box::new(crate::expressions::Subquery {
286                this,
287                alias: subquery.alias,
288                column_aliases: subquery.column_aliases,
289                alias_explicit_as: subquery.alias_explicit_as,
290                alias_keyword: subquery.alias_keyword,
291                order_by: subquery.order_by,
292                limit: subquery.limit,
293                offset: subquery.offset,
294                distribute_by: subquery.distribute_by,
295                sort_by: subquery.sort_by,
296                cluster_by: subquery.cluster_by,
297                lateral: subquery.lateral,
298                modifiers_inside: subquery.modifiers_inside,
299                trailing_comments: subquery.trailing_comments,
300                inferred_type: None,
301            }))
302        }
303
304        // Set operations
305        Expression::Union(union) => {
306            let mut u = *union;
307            let left = std::mem::replace(&mut u.left, Expression::Null(Null));
308            u.left = canonicalize_recursive(left, dialect);
309            let right = std::mem::replace(&mut u.right, Expression::Null(Null));
310            u.right = canonicalize_recursive(right, dialect);
311            Expression::Union(Box::new(u))
312        }
313        Expression::Intersect(intersect) => {
314            let mut i = *intersect;
315            let left = std::mem::replace(&mut i.left, Expression::Null(Null));
316            i.left = canonicalize_recursive(left, dialect);
317            let right = std::mem::replace(&mut i.right, Expression::Null(Null));
318            i.right = canonicalize_recursive(right, dialect);
319            Expression::Intersect(Box::new(i))
320        }
321        Expression::Except(except) => {
322            let mut e = *except;
323            let left = std::mem::replace(&mut e.left, Expression::Null(Null));
324            e.left = canonicalize_recursive(left, dialect);
325            let right = std::mem::replace(&mut e.right, Expression::Null(Null));
326            e.right = canonicalize_recursive(right, dialect);
327            Expression::Except(Box::new(e))
328        }
329
330        // Leaf nodes - return unchanged
331        other => other,
332    };
333
334    expr
335}
336
337/// Convert string addition to CONCAT.
338///
339/// When two TEXT types are added with +, convert to CONCAT.
340/// This is used by dialects like T-SQL and Redshift.
341fn add_text_to_concat(expression: Expression) -> Expression {
342    // In a full implementation, we would check if the operands are TEXT types
343    // and convert to CONCAT. For now, we return unchanged.
344    expression
345}
346
347/// Remove redundant cast expressions.
348///
349/// If casting to the same type the expression already is, remove the cast.
350fn remove_redundant_casts(expression: Expression) -> Expression {
351    if let Expression::Cast(cast) = &expression {
352        // Check if the inner expression's type matches the cast target
353        // In a full implementation with type annotations, we would compare types
354        // For now, just check simple cases
355
356        // If casting a literal to its natural type, we might be able to simplify
357        if let Expression::Literal(lit) = &cast.this {
358            if let Literal::String(_) = lit.as_ref() {
359                if matches!(&cast.to, DataType::VarChar { .. } | DataType::Text) {
360                    return cast.this.clone();
361                }
362            }
363        }
364        if let Expression::Literal(lit) = &cast.this {
365            if let Literal::Number(_) = lit.as_ref() {
366                if matches!(
367                    &cast.to,
368                    DataType::Int { .. }
369                        | DataType::BigInt { .. }
370                        | DataType::Decimal { .. }
371                        | DataType::Float { .. }
372                ) {
373                    // Could potentially remove cast, but be conservative
374                }
375            }
376        }
377    }
378    expression
379}
380
381/// Ensure expressions used as boolean predicates are actually boolean.
382///
383/// For example, in some dialects, integers can be used as booleans.
384/// This function ensures proper boolean semantics.
385fn ensure_bools(expression: Expression) -> Expression {
386    // In a full implementation, we would check if the expression is an integer
387    // and convert it to a comparison (e.g., x != 0).
388    // For now, return unchanged.
389    expression
390}
391
392/// Remove explicit ASC from ORDER BY clauses.
393///
394/// Since ASC is the default, `ORDER BY a ASC` can be simplified to `ORDER BY a`.
395fn remove_ascending_order(mut ordered: crate::expressions::Ordered) -> crate::expressions::Ordered {
396    // If ASC was explicitly written (not DESC), remove the explicit flag
397    // since ASC is the default ordering
398    if !ordered.desc && ordered.explicit_asc {
399        ordered.explicit_asc = false;
400    }
401    ordered
402}
403
404/// Canonicalize a binary comparison operation.
405fn canonicalize_comparison<F>(
406    constructor: F,
407    bin: crate::expressions::BinaryOp,
408    dialect: Option<DialectType>,
409) -> Expression
410where
411    F: FnOnce(Box<crate::expressions::BinaryOp>) -> Expression,
412{
413    let left = canonicalize_recursive(bin.left, dialect);
414    let right = canonicalize_recursive(bin.right, dialect);
415
416    // Check for date coercion opportunities
417    let (left, right) = coerce_date_operands(left, right);
418
419    constructor(Box::new(crate::expressions::BinaryOp {
420        left,
421        right,
422        left_comments: bin.left_comments,
423        operator_comments: bin.operator_comments,
424        trailing_comments: bin.trailing_comments,
425        inferred_type: None,
426    }))
427}
428
429/// Canonicalize a regular binary operation.
430fn canonicalize_binary<F>(
431    constructor: F,
432    bin: crate::expressions::BinaryOp,
433    dialect: Option<DialectType>,
434) -> Expression
435where
436    F: FnOnce(Box<crate::expressions::BinaryOp>) -> Expression,
437{
438    let left = canonicalize_recursive(bin.left, dialect);
439    let right = canonicalize_recursive(bin.right, dialect);
440
441    constructor(Box::new(crate::expressions::BinaryOp {
442        left,
443        right,
444        left_comments: bin.left_comments,
445        operator_comments: bin.operator_comments,
446        trailing_comments: bin.trailing_comments,
447        inferred_type: None,
448    }))
449}
450
451/// Coerce date operands in comparisons.
452///
453/// When comparing a date/datetime column with a string literal,
454/// add appropriate CAST to the string.
455fn coerce_date_operands(left: Expression, right: Expression) -> (Expression, Expression) {
456    // Check if we should cast string literals to date/datetime
457    let left = coerce_date_string(left, &right);
458    let right = coerce_date_string(right, &left);
459    (left, right)
460}
461
462/// Coerce a string literal to date/datetime if comparing with a temporal type.
463fn coerce_date_string(expr: Expression, _other: &Expression) -> Expression {
464    if let Expression::Literal(ref lit) = expr {
465        if let Literal::String(ref s) = lit.as_ref() {
466            // Check if the string is an ISO date or datetime
467            if is_iso_date(s) {
468                // In a full implementation, we would add CAST to DATE
469                // For now, return unchanged
470            } else if is_iso_datetime(s) {
471                // In a full implementation, we would add CAST to DATETIME/TIMESTAMP
472                // For now, return unchanged
473            }
474        }
475    }
476    expr
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482    use crate::generator::Generator;
483    use crate::parser::Parser;
484
485    fn gen(expr: &Expression) -> String {
486        Generator::new().generate(expr).unwrap()
487    }
488
489    fn parse(sql: &str) -> Expression {
490        Parser::parse_sql(sql).expect("Failed to parse")[0].clone()
491    }
492
493    #[test]
494    fn test_canonicalize_simple() {
495        let expr = parse("SELECT a FROM t");
496        let result = canonicalize(expr, None);
497        let sql = gen(&result);
498        assert!(sql.contains("SELECT"));
499    }
500
501    #[test]
502    fn test_canonicalize_preserves_structure() {
503        let expr = parse("SELECT a, b FROM t WHERE c = 1");
504        let result = canonicalize(expr, None);
505        let sql = gen(&result);
506        assert!(sql.contains("WHERE"));
507    }
508
509    #[test]
510    fn test_canonicalize_and_or() {
511        let expr = parse("SELECT 1 WHERE a AND b OR c");
512        let result = canonicalize(expr, None);
513        let sql = gen(&result);
514        assert!(sql.contains("AND") || sql.contains("OR"));
515    }
516
517    #[test]
518    fn test_canonicalize_comparison() {
519        let expr = parse("SELECT 1 WHERE a = 1 AND b > 2");
520        let result = canonicalize(expr, None);
521        let sql = gen(&result);
522        assert!(sql.contains("=") && sql.contains(">"));
523    }
524
525    #[test]
526    fn test_canonicalize_case() {
527        let expr = parse("SELECT CASE WHEN a = 1 THEN 'yes' ELSE 'no' END FROM t");
528        let result = canonicalize(expr, None);
529        let sql = gen(&result);
530        assert!(sql.contains("CASE") && sql.contains("WHEN"));
531    }
532
533    #[test]
534    fn test_canonicalize_subquery() {
535        let expr = parse("SELECT a FROM (SELECT b FROM t) AS sub");
536        let result = canonicalize(expr, None);
537        let sql = gen(&result);
538        assert!(sql.contains("SELECT") && sql.contains("sub"));
539    }
540
541    #[test]
542    fn test_canonicalize_order_by() {
543        let expr = parse("SELECT a FROM t ORDER BY a");
544        let result = canonicalize(expr, None);
545        let sql = gen(&result);
546        assert!(sql.contains("ORDER BY"));
547    }
548
549    #[test]
550    fn test_canonicalize_union() {
551        let expr = parse("SELECT a FROM t UNION SELECT b FROM s");
552        let result = canonicalize(expr, None);
553        let sql = gen(&result);
554        assert!(sql.contains("UNION"));
555    }
556
557    #[test]
558    fn test_add_text_to_concat_passthrough() {
559        // Test that non-text additions pass through
560        let expr = parse("SELECT 1 + 2");
561        let result = canonicalize(expr, None);
562        let sql = gen(&result);
563        assert!(sql.contains("+"));
564    }
565
566    #[test]
567    fn test_canonicalize_function() {
568        let expr = parse("SELECT MAX(a) FROM t");
569        let result = canonicalize(expr, None);
570        let sql = gen(&result);
571        assert!(sql.contains("MAX"));
572    }
573
574    #[test]
575    fn test_canonicalize_between() {
576        let expr = parse("SELECT 1 WHERE a BETWEEN 1 AND 10");
577        let result = canonicalize(expr, None);
578        let sql = gen(&result);
579        assert!(sql.contains("BETWEEN"));
580    }
581}