Skip to main content

polyglot_sql/optimizer/
normalize_identifiers.rs

1//! Identifier Normalization Module
2//!
3//! This module provides functionality for normalizing identifiers in SQL queries
4//! based on dialect-specific rules for case sensitivity and quoting.
5//!
6//! Ported from sqlglot's optimizer/normalize_identifiers.py
7
8use crate::dialects::DialectType;
9use crate::expressions::{Column, Expression, Identifier, Null};
10
11/// Strategy for normalizing identifiers based on dialect rules.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum NormalizationStrategy {
14    /// Unquoted identifiers are lowercased (e.g., PostgreSQL)
15    Lowercase,
16    /// Unquoted identifiers are uppercased (e.g., Oracle, Snowflake)
17    Uppercase,
18    /// Always case-sensitive, regardless of quotes (e.g., MySQL on Linux)
19    CaseSensitive,
20    /// Always case-insensitive (lowercase), regardless of quotes (e.g., Spark, BigQuery)
21    CaseInsensitive,
22    /// Always case-insensitive (uppercase), regardless of quotes
23    CaseInsensitiveUppercase,
24}
25
26impl Default for NormalizationStrategy {
27    fn default() -> Self {
28        Self::Lowercase
29    }
30}
31
32/// Get the normalization strategy for a dialect
33pub fn get_normalization_strategy(dialect: Option<DialectType>) -> NormalizationStrategy {
34    match dialect {
35        // Uppercase dialects
36        Some(DialectType::Oracle)
37        | Some(DialectType::Snowflake)
38        | Some(DialectType::Exasol)
39        | Some(DialectType::HANA) => NormalizationStrategy::Uppercase,
40        // Case-sensitive dialects
41        Some(DialectType::MySQL) | Some(DialectType::ClickHouse) => {
42            NormalizationStrategy::CaseSensitive
43        }
44        // Case-insensitive dialects (lowercase)
45        Some(DialectType::DuckDB)
46        | Some(DialectType::SQLite)
47        | Some(DialectType::BigQuery)
48        | Some(DialectType::Presto)
49        | Some(DialectType::Trino)
50        | Some(DialectType::Hive)
51        | Some(DialectType::Spark)
52        | Some(DialectType::Databricks)
53        | Some(DialectType::Redshift) => NormalizationStrategy::CaseInsensitive,
54        // Default: lowercase (PostgreSQL-like behavior)
55        _ => NormalizationStrategy::Lowercase,
56    }
57}
58
59/// Normalize identifiers in an expression based on dialect rules.
60///
61/// This transformation reflects how identifiers would be resolved by the engine
62/// corresponding to each SQL dialect. For example:
63/// - `FoO` → `foo` in PostgreSQL (lowercases unquoted)
64/// - `FoO` → `FOO` in Snowflake (uppercases unquoted)
65/// - `"FoO"` → `FoO` preserved when quoted (case-sensitive)
66///
67/// # Arguments
68/// * `expression` - The expression to normalize
69/// * `dialect` - The dialect to use for normalization rules
70///
71/// # Returns
72/// The expression with normalized identifiers
73pub fn normalize_identifiers(expression: Expression, dialect: Option<DialectType>) -> Expression {
74    let strategy = get_normalization_strategy(dialect);
75    normalize_expression(expression, strategy)
76}
77
78/// Normalize a single identifier based on the strategy.
79///
80/// # Arguments
81/// * `identifier` - The identifier to normalize
82/// * `strategy` - The normalization strategy to use
83///
84/// # Returns
85/// The normalized identifier
86pub fn normalize_identifier(identifier: Identifier, strategy: NormalizationStrategy) -> Identifier {
87    // Case-sensitive strategy: never normalize
88    if strategy == NormalizationStrategy::CaseSensitive {
89        return identifier;
90    }
91
92    // If quoted and not case-insensitive, don't normalize
93    if identifier.quoted
94        && strategy != NormalizationStrategy::CaseInsensitive
95        && strategy != NormalizationStrategy::CaseInsensitiveUppercase
96    {
97        return identifier;
98    }
99
100    // Normalize the identifier name
101    let normalized_name = match strategy {
102        NormalizationStrategy::Uppercase | NormalizationStrategy::CaseInsensitiveUppercase => {
103            identifier.name.to_uppercase()
104        }
105        NormalizationStrategy::Lowercase | NormalizationStrategy::CaseInsensitive => {
106            identifier.name.to_lowercase()
107        }
108        NormalizationStrategy::CaseSensitive => identifier.name, // Should not reach here
109    };
110
111    Identifier {
112        name: normalized_name,
113        quoted: identifier.quoted,
114        trailing_comments: identifier.trailing_comments,
115        span: None,
116    }
117}
118
119/// Recursively normalize all identifiers in an expression.
120fn normalize_expression(expression: Expression, strategy: NormalizationStrategy) -> Expression {
121    match expression {
122        Expression::Identifier(id) => Expression::Identifier(normalize_identifier(id, strategy)),
123        Expression::Column(col) => Expression::boxed_column(Column {
124            name: normalize_identifier(col.name, strategy),
125            table: col.table.map(|t| normalize_identifier(t, strategy)),
126            join_mark: col.join_mark,
127            trailing_comments: col.trailing_comments,
128            span: None,
129            inferred_type: None,
130        }),
131        Expression::Table(mut table) => {
132            table.name = normalize_identifier(table.name, strategy);
133            if let Some(schema) = table.schema {
134                table.schema = Some(normalize_identifier(schema, strategy));
135            }
136            if let Some(catalog) = table.catalog {
137                table.catalog = Some(normalize_identifier(catalog, strategy));
138            }
139            if let Some(alias) = table.alias {
140                table.alias = Some(normalize_identifier(alias, strategy));
141            }
142            table.column_aliases = table
143                .column_aliases
144                .into_iter()
145                .map(|a| normalize_identifier(a, strategy))
146                .collect();
147            Expression::Table(table)
148        }
149        Expression::Select(select) => {
150            let mut select = *select;
151            // Normalize SELECT expressions
152            select.expressions = select
153                .expressions
154                .into_iter()
155                .map(|e| normalize_expression(e, strategy))
156                .collect();
157            // Normalize FROM
158            if let Some(mut from) = select.from {
159                from.expressions = from
160                    .expressions
161                    .into_iter()
162                    .map(|e| normalize_expression(e, strategy))
163                    .collect();
164                select.from = Some(from);
165            }
166            // Normalize JOINs
167            select.joins = select
168                .joins
169                .into_iter()
170                .map(|mut j| {
171                    j.this = normalize_expression(j.this, strategy);
172                    if let Some(on) = j.on {
173                        j.on = Some(normalize_expression(on, strategy));
174                    }
175                    j
176                })
177                .collect();
178            // Normalize WHERE
179            if let Some(mut where_clause) = select.where_clause {
180                where_clause.this = normalize_expression(where_clause.this, strategy);
181                select.where_clause = Some(where_clause);
182            }
183            // Normalize GROUP BY
184            if let Some(mut group_by) = select.group_by {
185                group_by.expressions = group_by
186                    .expressions
187                    .into_iter()
188                    .map(|e| normalize_expression(e, strategy))
189                    .collect();
190                select.group_by = Some(group_by);
191            }
192            // Normalize HAVING
193            if let Some(mut having) = select.having {
194                having.this = normalize_expression(having.this, strategy);
195                select.having = Some(having);
196            }
197            // Normalize ORDER BY
198            if let Some(mut order_by) = select.order_by {
199                order_by.expressions = order_by
200                    .expressions
201                    .into_iter()
202                    .map(|mut o| {
203                        o.this = normalize_expression(o.this, strategy);
204                        o
205                    })
206                    .collect();
207                select.order_by = Some(order_by);
208            }
209            Expression::Select(Box::new(select))
210        }
211        Expression::Alias(alias) => {
212            let mut alias = *alias;
213            alias.this = normalize_expression(alias.this, strategy);
214            alias.alias = normalize_identifier(alias.alias, strategy);
215            Expression::Alias(Box::new(alias))
216        }
217        // Binary operations
218        Expression::And(bin) => normalize_binary(Expression::And, *bin, strategy),
219        Expression::Or(bin) => normalize_binary(Expression::Or, *bin, strategy),
220        Expression::Add(bin) => normalize_binary(Expression::Add, *bin, strategy),
221        Expression::Sub(bin) => normalize_binary(Expression::Sub, *bin, strategy),
222        Expression::Mul(bin) => normalize_binary(Expression::Mul, *bin, strategy),
223        Expression::Div(bin) => normalize_binary(Expression::Div, *bin, strategy),
224        Expression::Mod(bin) => normalize_binary(Expression::Mod, *bin, strategy),
225        Expression::Eq(bin) => normalize_binary(Expression::Eq, *bin, strategy),
226        Expression::Neq(bin) => normalize_binary(Expression::Neq, *bin, strategy),
227        Expression::Lt(bin) => normalize_binary(Expression::Lt, *bin, strategy),
228        Expression::Lte(bin) => normalize_binary(Expression::Lte, *bin, strategy),
229        Expression::Gt(bin) => normalize_binary(Expression::Gt, *bin, strategy),
230        Expression::Gte(bin) => normalize_binary(Expression::Gte, *bin, strategy),
231        Expression::Concat(bin) => normalize_binary(Expression::Concat, *bin, strategy),
232        // Unary operations
233        Expression::Not(un) => {
234            let mut un = *un;
235            un.this = normalize_expression(un.this, strategy);
236            Expression::Not(Box::new(un))
237        }
238        Expression::Neg(un) => {
239            let mut un = *un;
240            un.this = normalize_expression(un.this, strategy);
241            Expression::Neg(Box::new(un))
242        }
243        // Functions
244        Expression::Function(func) => {
245            let mut func = *func;
246            func.args = func
247                .args
248                .into_iter()
249                .map(|e| normalize_expression(e, strategy))
250                .collect();
251            func.qualified_name = func
252                .qualified_name
253                .into_iter()
254                .map(|i| normalize_identifier(i, strategy))
255                .collect();
256            if let Some(behavior) = &mut func.on_error {
257                behavior.value = behavior
258                    .value
259                    .take()
260                    .map(|e| normalize_expression(e, strategy));
261            }
262            Expression::Function(Box::new(func))
263        }
264        Expression::AggregateFunction(agg) => {
265            let mut agg = *agg;
266            agg.args = agg
267                .args
268                .into_iter()
269                .map(|e| normalize_expression(e, strategy))
270                .collect();
271            Expression::AggregateFunction(Box::new(agg))
272        }
273        // Other expressions with children
274        Expression::Paren(paren) => {
275            let mut paren = *paren;
276            paren.this = normalize_expression(paren.this, strategy);
277            Expression::Paren(Box::new(paren))
278        }
279        Expression::Case(case) => {
280            let mut case = *case;
281            case.operand = case.operand.map(|e| normalize_expression(e, strategy));
282            case.whens = case
283                .whens
284                .into_iter()
285                .map(|(w, t)| {
286                    (
287                        normalize_expression(w, strategy),
288                        normalize_expression(t, strategy),
289                    )
290                })
291                .collect();
292            case.else_ = case.else_.map(|e| normalize_expression(e, strategy));
293            Expression::Case(Box::new(case))
294        }
295        Expression::Cast(cast) => {
296            let mut cast = *cast;
297            cast.this = normalize_expression(cast.this, strategy);
298            Expression::Cast(Box::new(cast))
299        }
300        Expression::In(in_expr) => {
301            let mut in_expr = *in_expr;
302            in_expr.this = normalize_expression(in_expr.this, strategy);
303            in_expr.expressions = in_expr
304                .expressions
305                .into_iter()
306                .map(|e| normalize_expression(e, strategy))
307                .collect();
308            if let Some(q) = in_expr.query {
309                in_expr.query = Some(normalize_expression(q, strategy));
310            }
311            Expression::In(Box::new(in_expr))
312        }
313        Expression::Between(between) => {
314            let mut between = *between;
315            between.this = normalize_expression(between.this, strategy);
316            between.low = normalize_expression(between.low, strategy);
317            between.high = normalize_expression(between.high, strategy);
318            Expression::Between(Box::new(between))
319        }
320        Expression::Subquery(subquery) => {
321            let mut subquery = *subquery;
322            subquery.this = normalize_expression(subquery.this, strategy);
323            if let Some(alias) = subquery.alias {
324                subquery.alias = Some(normalize_identifier(alias, strategy));
325            }
326            Expression::Subquery(Box::new(subquery))
327        }
328        // Set operations
329        Expression::Union(mut union) => {
330            let left = std::mem::replace(&mut union.left, Expression::Null(Null));
331            union.left = normalize_expression(left, strategy);
332            let right = std::mem::replace(&mut union.right, Expression::Null(Null));
333            union.right = normalize_expression(right, strategy);
334            Expression::Union(union)
335        }
336        Expression::Intersect(mut intersect) => {
337            let left = std::mem::replace(&mut intersect.left, Expression::Null(Null));
338            intersect.left = normalize_expression(left, strategy);
339            let right = std::mem::replace(&mut intersect.right, Expression::Null(Null));
340            intersect.right = normalize_expression(right, strategy);
341            Expression::Intersect(intersect)
342        }
343        Expression::Except(mut except) => {
344            let left = std::mem::replace(&mut except.left, Expression::Null(Null));
345            except.left = normalize_expression(left, strategy);
346            let right = std::mem::replace(&mut except.right, Expression::Null(Null));
347            except.right = normalize_expression(right, strategy);
348            Expression::Except(except)
349        }
350        // Leaf nodes and others - return unchanged
351        _ => expression,
352    }
353}
354
355/// Helper to normalize binary operations
356fn normalize_binary<F>(
357    constructor: F,
358    mut bin: crate::expressions::BinaryOp,
359    strategy: NormalizationStrategy,
360) -> Expression
361where
362    F: FnOnce(Box<crate::expressions::BinaryOp>) -> Expression,
363{
364    bin.left = normalize_expression(bin.left, strategy);
365    bin.right = normalize_expression(bin.right, strategy);
366    constructor(Box::new(bin))
367}
368
369/// Check if an identifier contains case-sensitive characters based on dialect rules.
370pub fn is_case_sensitive(text: &str, strategy: NormalizationStrategy) -> bool {
371    match strategy {
372        NormalizationStrategy::CaseInsensitive
373        | NormalizationStrategy::CaseInsensitiveUppercase => false,
374        NormalizationStrategy::Uppercase => text.chars().any(|c| c.is_lowercase()),
375        NormalizationStrategy::Lowercase => text.chars().any(|c| c.is_uppercase()),
376        NormalizationStrategy::CaseSensitive => true,
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383    use crate::generator::Generator;
384    use crate::parser::Parser;
385
386    fn gen(expr: &Expression) -> String {
387        Generator::new().generate(expr).unwrap()
388    }
389
390    fn parse_and_normalize(sql: &str, dialect: Option<DialectType>) -> String {
391        let ast = Parser::parse_sql(sql).expect("Failed to parse");
392        let normalized = normalize_identifiers(ast[0].clone(), dialect);
393        gen(&normalized)
394    }
395
396    #[test]
397    fn test_normalize_lowercase() {
398        // PostgreSQL-like: lowercase unquoted identifiers
399        let result = parse_and_normalize("SELECT FoO FROM Bar", None);
400        assert!(result.contains("foo") || result.contains("FOO")); // normalized
401    }
402
403    #[test]
404    fn test_normalize_uppercase() {
405        // Snowflake: uppercase unquoted identifiers
406        let result = parse_and_normalize("SELECT foo FROM bar", Some(DialectType::Snowflake));
407        // Should contain uppercase versions
408        assert!(result.to_uppercase().contains("FOO"));
409    }
410
411    #[test]
412    fn test_normalize_preserves_quoted() {
413        // Quoted identifiers should be preserved in non-case-insensitive dialects
414        let id = Identifier {
415            name: "FoO".to_string(),
416            quoted: true,
417            trailing_comments: vec![],
418            span: None,
419        };
420        let normalized = normalize_identifier(id, NormalizationStrategy::Lowercase);
421        assert_eq!(normalized.name, "FoO"); // Preserved
422    }
423
424    #[test]
425    fn test_case_insensitive_normalizes_quoted() {
426        // In case-insensitive dialects, even quoted identifiers are normalized
427        let id = Identifier {
428            name: "FoO".to_string(),
429            quoted: true,
430            trailing_comments: vec![],
431            span: None,
432        };
433        let normalized = normalize_identifier(id, NormalizationStrategy::CaseInsensitive);
434        assert_eq!(normalized.name, "foo"); // Lowercased
435    }
436
437    #[test]
438    fn test_case_sensitive_no_normalization() {
439        // Case-sensitive dialects don't normalize at all
440        let id = Identifier {
441            name: "FoO".to_string(),
442            quoted: false,
443            trailing_comments: vec![],
444            span: None,
445        };
446        let normalized = normalize_identifier(id, NormalizationStrategy::CaseSensitive);
447        assert_eq!(normalized.name, "FoO"); // Unchanged
448    }
449
450    #[test]
451    fn test_normalize_column() {
452        let col = Expression::boxed_column(Column {
453            name: Identifier::new("MyColumn"),
454            table: Some(Identifier::new("MyTable")),
455            join_mark: false,
456            trailing_comments: vec![],
457            span: None,
458            inferred_type: None,
459        });
460
461        let normalized = normalize_expression(col, NormalizationStrategy::Lowercase);
462        let sql = gen(&normalized);
463        assert!(sql.contains("mycolumn") || sql.contains("mytable"));
464    }
465
466    #[test]
467    fn test_get_normalization_strategy() {
468        assert_eq!(
469            get_normalization_strategy(Some(DialectType::Snowflake)),
470            NormalizationStrategy::Uppercase
471        );
472        assert_eq!(
473            get_normalization_strategy(Some(DialectType::PostgreSQL)),
474            NormalizationStrategy::Lowercase
475        );
476        assert_eq!(
477            get_normalization_strategy(Some(DialectType::MySQL)),
478            NormalizationStrategy::CaseSensitive
479        );
480        assert_eq!(
481            get_normalization_strategy(Some(DialectType::DuckDB)),
482            NormalizationStrategy::CaseInsensitive
483        );
484    }
485}