Skip to main content

sqlglot_rust/dialects/
mod.rs

1use serde::{Deserialize, Serialize};
2
3use crate::ast::*;
4
5pub mod plugin;
6pub mod time;
7
8/// Supported SQL dialects.
9///
10/// Mirrors the full set of dialects supported by Python's sqlglot library.
11/// Dialects are grouped into **Official** (core, higher-priority maintenance)
12/// and **Community** (contributed, fully functional) tiers.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub enum Dialect {
15    // ── Core / base ──────────────────────────────────────────────────────
16    /// ANSI SQL standard (default / base dialect)
17    Ansi,
18
19    // ── Official dialects ────────────────────────────────────────────────
20    /// AWS Athena (Presto-based)
21    Athena,
22    /// Google BigQuery
23    BigQuery,
24    /// ClickHouse
25    ClickHouse,
26    /// Databricks (Spark-based)
27    Databricks,
28    /// DuckDB
29    DuckDb,
30    /// Apache Hive
31    Hive,
32    /// MySQL
33    Mysql,
34    /// Oracle Database
35    Oracle,
36    /// PostgreSQL
37    Postgres,
38    /// Presto
39    Presto,
40    /// Amazon Redshift (Postgres-based)
41    Redshift,
42    /// Snowflake
43    Snowflake,
44    /// Apache Spark SQL
45    Spark,
46    /// SQLite
47    Sqlite,
48    /// StarRocks (MySQL-compatible)
49    StarRocks,
50    /// Trino (Presto successor)
51    Trino,
52    /// Microsoft SQL Server (T-SQL)
53    Tsql,
54
55    // ── Community dialects ───────────────────────────────────────────────
56    /// Apache Doris (MySQL-compatible)
57    Doris,
58    /// Dremio
59    Dremio,
60    /// Apache Drill
61    Drill,
62    /// Apache Druid
63    Druid,
64    /// Exasol
65    Exasol,
66    /// Microsoft Fabric (T-SQL variant)
67    Fabric,
68    /// Materialize (Postgres-compatible)
69    Materialize,
70    /// PRQL (Pipelined Relational Query Language)
71    Prql,
72    /// RisingWave (Postgres-compatible)
73    RisingWave,
74    /// SingleStore (MySQL-compatible)
75    SingleStore,
76    /// Tableau
77    Tableau,
78    /// Teradata
79    Teradata,
80}
81
82impl std::fmt::Display for Dialect {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        match self {
85            Dialect::Ansi => write!(f, "ANSI SQL"),
86            Dialect::Athena => write!(f, "Athena"),
87            Dialect::BigQuery => write!(f, "BigQuery"),
88            Dialect::ClickHouse => write!(f, "ClickHouse"),
89            Dialect::Databricks => write!(f, "Databricks"),
90            Dialect::DuckDb => write!(f, "DuckDB"),
91            Dialect::Hive => write!(f, "Hive"),
92            Dialect::Mysql => write!(f, "MySQL"),
93            Dialect::Oracle => write!(f, "Oracle"),
94            Dialect::Postgres => write!(f, "PostgreSQL"),
95            Dialect::Presto => write!(f, "Presto"),
96            Dialect::Redshift => write!(f, "Redshift"),
97            Dialect::Snowflake => write!(f, "Snowflake"),
98            Dialect::Spark => write!(f, "Spark"),
99            Dialect::Sqlite => write!(f, "SQLite"),
100            Dialect::StarRocks => write!(f, "StarRocks"),
101            Dialect::Trino => write!(f, "Trino"),
102            Dialect::Tsql => write!(f, "T-SQL"),
103            Dialect::Doris => write!(f, "Doris"),
104            Dialect::Dremio => write!(f, "Dremio"),
105            Dialect::Drill => write!(f, "Drill"),
106            Dialect::Druid => write!(f, "Druid"),
107            Dialect::Exasol => write!(f, "Exasol"),
108            Dialect::Fabric => write!(f, "Fabric"),
109            Dialect::Materialize => write!(f, "Materialize"),
110            Dialect::Prql => write!(f, "PRQL"),
111            Dialect::RisingWave => write!(f, "RisingWave"),
112            Dialect::SingleStore => write!(f, "SingleStore"),
113            Dialect::Tableau => write!(f, "Tableau"),
114            Dialect::Teradata => write!(f, "Teradata"),
115        }
116    }
117}
118
119impl Dialect {
120    /// Returns the support tier for this dialect.
121    #[must_use]
122    pub fn support_level(&self) -> &'static str {
123        match self {
124            Dialect::Ansi
125            | Dialect::Athena
126            | Dialect::BigQuery
127            | Dialect::ClickHouse
128            | Dialect::Databricks
129            | Dialect::DuckDb
130            | Dialect::Hive
131            | Dialect::Mysql
132            | Dialect::Oracle
133            | Dialect::Postgres
134            | Dialect::Presto
135            | Dialect::Redshift
136            | Dialect::Snowflake
137            | Dialect::Spark
138            | Dialect::Sqlite
139            | Dialect::StarRocks
140            | Dialect::Trino
141            | Dialect::Tsql => "Official",
142
143            Dialect::Doris
144            | Dialect::Dremio
145            | Dialect::Drill
146            | Dialect::Druid
147            | Dialect::Exasol
148            | Dialect::Fabric
149            | Dialect::Materialize
150            | Dialect::Prql
151            | Dialect::RisingWave
152            | Dialect::SingleStore
153            | Dialect::Tableau
154            | Dialect::Teradata => "Community",
155        }
156    }
157
158    /// Returns all dialect variants.
159    #[must_use]
160    pub fn all() -> &'static [Dialect] {
161        &[
162            Dialect::Ansi,
163            Dialect::Athena,
164            Dialect::BigQuery,
165            Dialect::ClickHouse,
166            Dialect::Databricks,
167            Dialect::Doris,
168            Dialect::Dremio,
169            Dialect::Drill,
170            Dialect::Druid,
171            Dialect::DuckDb,
172            Dialect::Exasol,
173            Dialect::Fabric,
174            Dialect::Hive,
175            Dialect::Materialize,
176            Dialect::Mysql,
177            Dialect::Oracle,
178            Dialect::Postgres,
179            Dialect::Presto,
180            Dialect::Prql,
181            Dialect::Redshift,
182            Dialect::RisingWave,
183            Dialect::SingleStore,
184            Dialect::Snowflake,
185            Dialect::Spark,
186            Dialect::Sqlite,
187            Dialect::StarRocks,
188            Dialect::Tableau,
189            Dialect::Teradata,
190            Dialect::Trino,
191            Dialect::Tsql,
192        ]
193    }
194
195    /// Parse a dialect name (case-insensitive) into a `Dialect`.
196    pub fn from_str(s: &str) -> Option<Dialect> {
197        match s.to_lowercase().as_str() {
198            "" | "ansi" => Some(Dialect::Ansi),
199            "athena" => Some(Dialect::Athena),
200            "bigquery" => Some(Dialect::BigQuery),
201            "clickhouse" => Some(Dialect::ClickHouse),
202            "databricks" => Some(Dialect::Databricks),
203            "doris" => Some(Dialect::Doris),
204            "dremio" => Some(Dialect::Dremio),
205            "drill" => Some(Dialect::Drill),
206            "druid" => Some(Dialect::Druid),
207            "duckdb" => Some(Dialect::DuckDb),
208            "exasol" => Some(Dialect::Exasol),
209            "fabric" => Some(Dialect::Fabric),
210            "hive" => Some(Dialect::Hive),
211            "materialize" => Some(Dialect::Materialize),
212            "mysql" => Some(Dialect::Mysql),
213            "oracle" => Some(Dialect::Oracle),
214            "postgres" | "postgresql" => Some(Dialect::Postgres),
215            "presto" => Some(Dialect::Presto),
216            "prql" => Some(Dialect::Prql),
217            "redshift" => Some(Dialect::Redshift),
218            "risingwave" => Some(Dialect::RisingWave),
219            "singlestore" => Some(Dialect::SingleStore),
220            "snowflake" => Some(Dialect::Snowflake),
221            "spark" => Some(Dialect::Spark),
222            "sqlite" => Some(Dialect::Sqlite),
223            "starrocks" => Some(Dialect::StarRocks),
224            "tableau" => Some(Dialect::Tableau),
225            "teradata" => Some(Dialect::Teradata),
226            "trino" => Some(Dialect::Trino),
227            "tsql" | "mssql" | "sqlserver" => Some(Dialect::Tsql),
228            _ => None,
229        }
230    }
231}
232
233// ═══════════════════════════════════════════════════════════════════════════
234// Dialect families — helpers for grouping similar dialects
235// ═══════════════════════════════════════════════════════════════════════════
236
237/// Dialects in the MySQL family (use SUBSTR, IFNULL, similar type system).
238fn is_mysql_family(d: Dialect) -> bool {
239    matches!(
240        d,
241        Dialect::Mysql | Dialect::Doris | Dialect::SingleStore | Dialect::StarRocks
242    )
243}
244
245/// Dialects in the Postgres family (support ILIKE, BYTEA, SUBSTRING).
246fn is_postgres_family(d: Dialect) -> bool {
247    matches!(
248        d,
249        Dialect::Postgres | Dialect::Redshift | Dialect::Materialize | Dialect::RisingWave
250    )
251}
252
253/// Dialects in the Presto family (ANSI-like, VARCHAR oriented).
254fn is_presto_family(d: Dialect) -> bool {
255    matches!(d, Dialect::Presto | Dialect::Trino | Dialect::Athena)
256}
257
258/// Dialects in the Hive/Spark family (use STRING type, SUBSTR).
259fn is_hive_family(d: Dialect) -> bool {
260    matches!(d, Dialect::Hive | Dialect::Spark | Dialect::Databricks)
261}
262
263/// Dialects in the T-SQL family.
264pub(crate) fn is_tsql_family(d: Dialect) -> bool {
265    matches!(d, Dialect::Tsql | Dialect::Fabric)
266}
267
268/// Returns `true` when `name` (case-insensitive) is a T-SQL reserved keyword
269/// that must be quoted with square brackets when used as an identifier
270/// (e.g. as a column alias, table alias, or column name).
271///
272/// Sourced from Microsoft's documented T-SQL reserved words (current and
273/// future). Covers both the ANSI/ODBC reserved set and SQL Server's
274/// dialect-specific reservations. Not exhaustive for every contextual
275/// keyword — focuses on words that, when emitted unquoted as aliases, will
276/// cause MSSQL syntax error 156.
277#[must_use]
278pub(crate) fn is_tsql_reserved(name: &str) -> bool {
279    // Reserved word set — keep in sorted order for binary_search.
280    // Source: docs.microsoft.com "Reserved Keywords (Transact-SQL)" and
281    // "ODBC Reserved Keywords", plus ANSI/ISO future reserved words.
282    const RESERVED: &[&str] = &[
283        "ABSOLUTE",
284        "ACTION",
285        "ADA",
286        "ADD",
287        "ALL",
288        "ALLOCATE",
289        "ALTER",
290        "AND",
291        "ANY",
292        "ARE",
293        "AS",
294        "ASC",
295        "ASSERTION",
296        "AT",
297        "AUTHORIZATION",
298        "AVG",
299        "BACKUP",
300        "BEGIN",
301        "BETWEEN",
302        "BIT",
303        "BIT_LENGTH",
304        "BOTH",
305        "BREAK",
306        "BROWSE",
307        "BULK",
308        "BY",
309        "CASCADE",
310        "CASCADED",
311        "CASE",
312        "CAST",
313        "CATALOG",
314        "CHAR",
315        "CHARACTER",
316        "CHARACTER_LENGTH",
317        "CHAR_LENGTH",
318        "CHECK",
319        "CHECKPOINT",
320        "CLOSE",
321        "CLUSTERED",
322        "COALESCE",
323        "COLLATE",
324        "COLLATION",
325        "COLUMN",
326        "COMMIT",
327        "COMPUTE",
328        "CONNECT",
329        "CONNECTION",
330        "CONSTRAINT",
331        "CONSTRAINTS",
332        "CONTAINS",
333        "CONTAINSTABLE",
334        "CONTINUE",
335        "CONVERT",
336        "CORRESPONDING",
337        "COUNT",
338        "CREATE",
339        "CROSS",
340        "CURRENT",
341        "CURRENT_DATE",
342        "CURRENT_TIME",
343        "CURRENT_TIMESTAMP",
344        "CURRENT_USER",
345        "CURSOR",
346        "DATABASE",
347        "DATE",
348        "DBCC",
349        "DEALLOCATE",
350        "DEC",
351        "DECIMAL",
352        "DECLARE",
353        "DEFAULT",
354        "DEFERRABLE",
355        "DEFERRED",
356        "DELETE",
357        "DENY",
358        "DESC",
359        "DESCRIBE",
360        "DESCRIPTOR",
361        "DIAGNOSTICS",
362        "DISCONNECT",
363        "DISK",
364        "DISTINCT",
365        "DISTRIBUTED",
366        "DOMAIN",
367        "DOUBLE",
368        "DROP",
369        "DUMP",
370        "ELSE",
371        "END",
372        "ERRLVL",
373        "ESCAPE",
374        "EXCEPT",
375        "EXCEPTION",
376        "EXEC",
377        "EXECUTE",
378        "EXISTS",
379        "EXIT",
380        "EXTERNAL",
381        "EXTRACT",
382        "FETCH",
383        "FILE",
384        "FILLFACTOR",
385        "FLOAT",
386        "FOR",
387        "FOREIGN",
388        "FORTRAN",
389        "FOUND",
390        "FREETEXT",
391        "FREETEXTTABLE",
392        "FROM",
393        "FULL",
394        "FUNCTION",
395        "GET",
396        "GLOBAL",
397        "GO",
398        "GOTO",
399        "GRANT",
400        "GROUP",
401        "HAVING",
402        "HOLDLOCK",
403        "HOUR",
404        "IDENTITY",
405        "IDENTITYCOL",
406        "IDENTITY_INSERT",
407        "IF",
408        "IMMEDIATE",
409        "IN",
410        "INCLUDE",
411        "INDEX",
412        "INDICATOR",
413        "INITIALLY",
414        "INNER",
415        "INPUT",
416        "INSENSITIVE",
417        "INSERT",
418        "INT",
419        "INTEGER",
420        "INTERSECT",
421        "INTERVAL",
422        "INTO",
423        "IS",
424        "ISOLATION",
425        "JOIN",
426        "KEY",
427        "KILL",
428        "LANGUAGE",
429        "LAST",
430        "LEADING",
431        "LEFT",
432        "LEVEL",
433        "LIKE",
434        "LINENO",
435        "LOAD",
436        "LOCAL",
437        "LOWER",
438        "MATCH",
439        "MAX",
440        "MERGE",
441        "MIN",
442        "MINUTE",
443        "MODULE",
444        "MONTH",
445        "NAMES",
446        "NATIONAL",
447        "NATURAL",
448        "NCHAR",
449        "NEXT",
450        "NO",
451        "NOCHECK",
452        "NONCLUSTERED",
453        "NONE",
454        "NOT",
455        "NULL",
456        "NULLIF",
457        "NUMERIC",
458        "OCTET_LENGTH",
459        "OF",
460        "OFF",
461        "OFFSETS",
462        "ON",
463        "ONLY",
464        "OPEN",
465        "OPENDATASOURCE",
466        "OPENQUERY",
467        "OPENROWSET",
468        "OPENXML",
469        "OPTION",
470        "OR",
471        "ORDER",
472        "OUTER",
473        "OUTPUT",
474        "OVER",
475        "OVERLAPS",
476        "PAD",
477        "PARTIAL",
478        "PASCAL",
479        "PERCENT",
480        "PIVOT",
481        "PLAN",
482        "POSITION",
483        "PRECISION",
484        "PREPARE",
485        "PRESERVE",
486        "PRIMARY",
487        "PRINT",
488        "PRIOR",
489        "PRIVILEGES",
490        "PROC",
491        "PROCEDURE",
492        "PUBLIC",
493        "RAISERROR",
494        "READ",
495        "READTEXT",
496        "REAL",
497        "RECONFIGURE",
498        "REFERENCES",
499        "RELATIVE",
500        "REPLICATION",
501        "RESTORE",
502        "RESTRICT",
503        "RETURN",
504        "REVERT",
505        "REVOKE",
506        "RIGHT",
507        "ROLLBACK",
508        "ROWCOUNT",
509        "ROWGUIDCOL",
510        "ROWS",
511        "RULE",
512        "SAVE",
513        "SCHEMA",
514        "SCROLL",
515        "SECOND",
516        "SECTION",
517        "SECURITYAUDIT",
518        "SELECT",
519        "SEMANTICKEYPHRASETABLE",
520        "SEMANTICSIMILARITYDETAILSTABLE",
521        "SEMANTICSIMILARITYTABLE",
522        "SESSION",
523        "SESSION_USER",
524        "SET",
525        "SETUSER",
526        "SHUTDOWN",
527        "SIZE",
528        "SMALLINT",
529        "SOME",
530        "SPACE",
531        "SQL",
532        "SQLCA",
533        "SQLCODE",
534        "SQLERROR",
535        "SQLSTATE",
536        "SQLWARNING",
537        "STATISTICS",
538        "SUBSTRING",
539        "SUM",
540        "SYSTEM_USER",
541        "TABLE",
542        "TABLESAMPLE",
543        "TEMPORARY",
544        "TEXTSIZE",
545        "THEN",
546        "TIME",
547        "TIMESTAMP",
548        "TIMEZONE_HOUR",
549        "TIMEZONE_MINUTE",
550        "TO",
551        "TOP",
552        "TRAILING",
553        "TRAN",
554        "TRANSACTION",
555        "TRANSLATE",
556        "TRANSLATION",
557        "TRIGGER",
558        "TRIM",
559        "TRUE",
560        "TRUNCATE",
561        "TRY_CONVERT",
562        "TSEQUAL",
563        "UNION",
564        "UNIQUE",
565        "UNKNOWN",
566        "UNPIVOT",
567        "UPDATE",
568        "UPDATETEXT",
569        "UPPER",
570        "USAGE",
571        "USE",
572        "USER",
573        "USING",
574        "VALUE",
575        "VALUES",
576        "VARCHAR",
577        "VARYING",
578        "VIEW",
579        "WAITFOR",
580        "WHEN",
581        "WHENEVER",
582        "WHERE",
583        "WHILE",
584        "WITH",
585        "WITHIN GROUP",
586        "WORK",
587        "WRITE",
588        "WRITETEXT",
589        "YEAR",
590        "ZONE",
591    ];
592
593    // Cheap upper-case comparison without allocation for ASCII identifiers.
594    if name.is_empty() || name.len() > 32 {
595        return false;
596    }
597    let mut buf = [0u8; 32];
598    for (i, b) in name.as_bytes().iter().enumerate() {
599        buf[i] = b.to_ascii_uppercase();
600    }
601    let upper = match std::str::from_utf8(&buf[..name.len()]) {
602        Ok(s) => s,
603        Err(_) => return false,
604    };
605    RESERVED.binary_search(&upper).is_ok()
606}
607
608/// Dialects that natively support ILIKE.
609pub(crate) fn supports_ilike_builtin(d: Dialect) -> bool {
610    matches!(
611        d,
612        Dialect::Postgres
613            | Dialect::Redshift
614            | Dialect::Materialize
615            | Dialect::RisingWave
616            | Dialect::DuckDb
617            | Dialect::Snowflake
618            | Dialect::ClickHouse
619            | Dialect::Trino
620            | Dialect::Presto
621            | Dialect::Athena
622            | Dialect::Databricks
623            | Dialect::Spark
624            | Dialect::Hive
625            | Dialect::StarRocks
626            | Dialect::Exasol
627            | Dialect::Druid
628            | Dialect::Dremio
629    )
630}
631
632// ═══════════════════════════════════════════════════════════════════════════
633// Statement / expression transforms
634// ═══════════════════════════════════════════════════════════════════════════
635
636/// Transform a statement from one dialect to another.
637///
638/// This applies dialect-specific rewrite rules such as:
639/// - Type mapping (e.g., `TEXT` → `STRING` for BigQuery)
640/// - Function name mapping (e.g., `NOW()` → `CURRENT_TIMESTAMP()`)
641/// - ILIKE → LIKE with LOWER() wrapping for dialects that don't support ILIKE
642#[must_use]
643pub fn transform(statement: &Statement, from: Dialect, to: Dialect) -> Statement {
644    if from == to {
645        return statement.clone();
646    }
647    let mut stmt = statement.clone();
648    transform_statement(&mut stmt, to);
649    stmt
650}
651
652fn transform_statement(statement: &mut Statement, target: Dialect) {
653    match statement {
654        Statement::Select(sel) => {
655            // Transform LIMIT / TOP / FETCH FIRST for the target dialect
656            transform_limit(sel, target);
657            // Transform identifier quoting for the target dialect
658            transform_quotes_in_select(sel, target);
659
660            for item in &mut sel.columns {
661                if let SelectItem::Expr { expr, .. } = item {
662                    *expr = transform_expr(expr.clone(), target);
663                }
664            }
665            if let Some(wh) = &mut sel.where_clause {
666                *wh = transform_expr(wh.clone(), target);
667            }
668            for gb in &mut sel.group_by {
669                *gb = transform_expr(gb.clone(), target);
670            }
671            if let Some(having) = &mut sel.having {
672                *having = transform_expr(having.clone(), target);
673            }
674        }
675        Statement::Insert(ins) => {
676            if let InsertSource::Values(rows) = &mut ins.source {
677                for row in rows {
678                    for val in row {
679                        *val = transform_expr(val.clone(), target);
680                    }
681                }
682            }
683            // Transform RETURNING expressions
684            for item in &mut ins.returning {
685                if let SelectItem::Expr { expr, .. } = item {
686                    *expr = transform_expr(expr.clone(), target);
687                }
688            }
689        }
690        Statement::Update(upd) => {
691            for (_, val) in &mut upd.assignments {
692                *val = transform_expr(val.clone(), target);
693            }
694            if let Some(wh) = &mut upd.where_clause {
695                *wh = transform_expr(wh.clone(), target);
696            }
697            // Transform RETURNING expressions
698            for item in &mut upd.returning {
699                if let SelectItem::Expr { expr, .. } = item {
700                    *expr = transform_expr(expr.clone(), target);
701                }
702            }
703        }
704        Statement::Delete(del) => {
705            if let Some(wh) = &mut del.where_clause {
706                *wh = transform_expr(wh.clone(), target);
707            }
708            // Transform RETURNING expressions
709            for item in &mut del.returning {
710                if let SelectItem::Expr { expr, .. } = item {
711                    *expr = transform_expr(expr.clone(), target);
712                }
713            }
714        }
715        // DDL: map data types in CREATE TABLE column definitions
716        Statement::CreateTable(ct) => {
717            for col in &mut ct.columns {
718                col.data_type = map_data_type(col.data_type.clone(), target);
719                if let Some(default) = &mut col.default {
720                    *default = transform_expr(default.clone(), target);
721                }
722            }
723            // Transform constraints (CHECK expressions)
724            for constraint in &mut ct.constraints {
725                if let TableConstraint::Check { expr, .. } = constraint {
726                    *expr = transform_expr(expr.clone(), target);
727                }
728            }
729            // Transform AS SELECT subquery
730            if let Some(as_select) = &mut ct.as_select {
731                transform_statement(as_select, target);
732            }
733        }
734        // DDL: map data types in ALTER TABLE ADD COLUMN
735        Statement::AlterTable(alt) => {
736            for action in &mut alt.actions {
737                match action {
738                    AlterTableAction::AddColumn(col) => {
739                        col.data_type = map_data_type(col.data_type.clone(), target);
740                        if let Some(default) = &mut col.default {
741                            *default = transform_expr(default.clone(), target);
742                        }
743                    }
744                    AlterTableAction::AlterColumnType { data_type, .. } => {
745                        *data_type = map_data_type(data_type.clone(), target);
746                    }
747                    _ => {}
748                }
749            }
750        }
751        _ => {}
752    }
753}
754
755/// Returns `true` when `expr` is already a zero-guarded divisor — either an
756/// `Expr::NullIf { .., r#else: 0 }` node or a `NULLIF(<x>, 0)` function call
757/// (the parser lowers `NULLIF(...)` to `Expr::Function`). Used by the CR-019
758/// safe-divide transform to avoid emitting a redundant
759/// `NULLIF(NULLIF(<x>, 0), 0)` when the source already guarded the divisor.
760fn is_zero_guarded_divisor(expr: &Expr) -> bool {
761    let is_zero = |e: &Expr| matches!(e, Expr::Number(n) if n == "0");
762    match expr {
763        Expr::NullIf { r#else, .. } => is_zero(r#else),
764        Expr::Function { name, args, .. } => {
765            name.eq_ignore_ascii_case("NULLIF") && args.len() == 2 && is_zero(&args[1])
766        }
767        _ => false,
768    }
769}
770
771/// Transform an expression for the target dialect.
772fn transform_expr(expr: Expr, target: Dialect) -> Expr {
773    match expr {
774        // Map function names across dialects
775        Expr::Function {
776            name,
777            args,
778            distinct,
779            filter,
780            over,
781            order_by,
782            within_group,
783        } => {
784            let new_name = map_function_name(&name, target);
785            let new_args: Vec<Expr> = args
786                .into_iter()
787                .map(|a| transform_expr(a, target))
788                .collect();
789            Expr::Function {
790                name: new_name,
791                args: new_args,
792                distinct,
793                filter: filter.map(|f| Box::new(transform_expr(*f, target))),
794                over,
795                order_by,
796                within_group,
797            }
798        }
799        // Recurse into typed function child expressions, with special handling
800        // for date/time formatting functions that need format string conversion
801        Expr::TypedFunction { func, filter, over } => {
802            let transformed_func = transform_typed_function(func, target);
803            Expr::TypedFunction {
804                func: transformed_func,
805                filter: filter.map(|f| Box::new(transform_expr(*f, target))),
806                over,
807            }
808        }
809        // ILIKE → LOWER(expr) LIKE LOWER(pattern) for non-supporting dialects
810        Expr::ILike {
811            expr,
812            pattern,
813            negated,
814            escape,
815        } if !supports_ilike_builtin(target) => Expr::Like {
816            expr: Box::new(Expr::TypedFunction {
817                func: TypedFunction::Lower {
818                    expr: Box::new(transform_expr(*expr, target)),
819                },
820                filter: None,
821                over: None,
822            }),
823            pattern: Box::new(Expr::TypedFunction {
824                func: TypedFunction::Lower {
825                    expr: Box::new(transform_expr(*pattern, target)),
826                },
827                filter: None,
828                over: None,
829            }),
830            negated,
831            escape,
832        },
833        // SIMILAR TO → LIKE for T-SQL (lossy: regex features dropped)
834        Expr::SimilarTo {
835            expr,
836            pattern,
837            negated,
838            escape,
839        } if is_tsql_family(target) => {
840            let transformed_pattern = transform_expr(*pattern, target);
841            let simplified = simplify_similar_to_pattern(&transformed_pattern);
842            Expr::Like {
843                expr: Box::new(transform_expr(*expr, target)),
844                pattern: Box::new(simplified),
845                negated,
846                escape,
847            }
848        }
849        // Map data types in CAST
850        Expr::Cast { expr, data_type } => Expr::Cast {
851            expr: Box::new(transform_expr(*expr, target)),
852            data_type: map_data_type(data_type, target),
853        },
854        // Recurse into binary ops, with T-SQL specific transforms
855        Expr::BinaryOp { left, op, right } => {
856            // Change 3: || → CONCAT() for T-SQL
857            // Collect args BEFORE recursive transform to flatten the full chain
858            if op == BinaryOperator::Concat && is_tsql_family(target) {
859                let mut args = Vec::new();
860                collect_concat_args(
861                    &Expr::BinaryOp {
862                        left,
863                        op: BinaryOperator::Concat,
864                        right,
865                    },
866                    &mut args,
867                );
868                // Now transform each collected arg
869                let args = args
870                    .into_iter()
871                    .map(|a| transform_expr(a, target))
872                    .collect();
873                return Expr::Function {
874                    name: "CONCAT".to_string(),
875                    args,
876                    distinct: false,
877                    filter: None,
878                    over: None,
879                    order_by: vec![],
880                    within_group: false,
881                };
882            }
883
884            let left_transformed = transform_expr(*left, target);
885            let right_transformed = transform_expr(*right, target);
886
887            // Change 6: expr ± INTERVAL → DATEADD() for T-SQL
888            if is_tsql_family(target) && matches!(op, BinaryOperator::Plus | BinaryOperator::Minus)
889            {
890                if let Some(dateadd) =
891                    try_transform_interval_arithmetic(&left_transformed, &op, &right_transformed)
892                {
893                    return dateadd;
894                }
895            }
896
897            // Change 7 (CR-019): a / b → a / NULLIF(b, 0) for the T-SQL family,
898            // so a zero divisor yields NULL instead of raising "Divide by zero"
899            // (code 8134). SQL Server does not short-circuit the ANDed WHERE qual
900            // list the way PostgreSQL does, so a guard such as `WHERE b <> 0`
901            // cannot be relied on to protect the division — the guard must live
902            // in the expression itself. NULLIF(b, 0) returns b unchanged whenever
903            // b <> 0, so non-zero divisors are behaviorally unaffected. Modulo is
904            // included because `x % 0` raises the same 8134 error class. A divisor
905            // already written as NULLIF(<x>, 0) is left as-is (no double wrap).
906            if is_tsql_family(target)
907                && matches!(op, BinaryOperator::Divide | BinaryOperator::Modulo)
908                && !is_zero_guarded_divisor(&right_transformed)
909            {
910                return Expr::BinaryOp {
911                    left: Box::new(left_transformed),
912                    op,
913                    right: Box::new(Expr::NullIf {
914                        expr: Box::new(right_transformed),
915                        r#else: Box::new(Expr::Number("0".to_string())),
916                    }),
917                };
918            }
919
920            Expr::BinaryOp {
921                left: Box::new(left_transformed),
922                op,
923                right: Box::new(right_transformed),
924            }
925        }
926        Expr::UnaryOp { op, expr } => Expr::UnaryOp {
927            op,
928            expr: Box::new(transform_expr(*expr, target)),
929        },
930        Expr::Nested(inner) => Expr::Nested(Box::new(transform_expr(*inner, target))),
931        // Transform quoting on column references
932        Expr::Column {
933            table,
934            name,
935            quote_style,
936            table_quote_style,
937        } => {
938            let new_qs = if quote_style.is_quoted() {
939                QuoteStyle::for_dialect(target)
940            } else {
941                QuoteStyle::None
942            };
943            let new_tqs = if table_quote_style.is_quoted() {
944                QuoteStyle::for_dialect(target)
945            } else {
946                QuoteStyle::None
947            };
948            Expr::Column {
949                table,
950                name,
951                quote_style: new_qs,
952                table_quote_style: new_tqs,
953            }
954        }
955        // Everything else stays the same
956        other => other,
957    }
958}
959
960// ═══════════════════════════════════════════════════════════════════════════
961// Typed function transformation with format string conversion
962// ═══════════════════════════════════════════════════════════════════════════
963
964/// Transform a TypedFunction, including date/time format string conversion.
965///
966/// For TimeToStr and StrToTime functions, this converts the format string
967/// from the source dialect's convention to the target dialect's convention.
968fn transform_typed_function(func: TypedFunction, target: Dialect) -> TypedFunction {
969    match func {
970        TypedFunction::TimeToStr { expr, format } => {
971            let transformed_expr = Box::new(transform_expr(*expr, target));
972            let transformed_format = transform_format_expr(*format, target);
973            TypedFunction::TimeToStr {
974                expr: transformed_expr,
975                format: Box::new(transformed_format),
976            }
977        }
978        TypedFunction::StrToTime { expr, format } => {
979            let transformed_expr = Box::new(transform_expr(*expr, target));
980            let transformed_format = transform_format_expr(*format, target);
981            TypedFunction::StrToTime {
982                expr: transformed_expr,
983                format: Box::new(transformed_format),
984            }
985        }
986        // For all other typed functions, just transform child expressions
987        other => other.transform_children(&|e| transform_expr(e, target)),
988    }
989}
990
991/// Transform a format string expression for the target dialect.
992///
993/// If the expression is a string literal, convert the format specifiers.
994/// Otherwise, just recursively transform child expressions.
995fn transform_format_expr(expr: Expr, target: Dialect) -> Expr {
996    // We need to know the source dialect to convert properly.
997    // Since we don't have access to the source dialect here, we use heuristics
998    // to detect the format style based on the format string content.
999    match &expr {
1000        Expr::StringLiteral(s) | Expr::NationalStringLiteral(s) => {
1001            let detected_source = detect_format_style(s);
1002            let target_style = time::TimeFormatStyle::for_dialect(target);
1003
1004            // Only convert if styles differ
1005            if detected_source != target_style {
1006                let converted = time::format_time(s, detected_source, target_style);
1007                match expr {
1008                    Expr::NationalStringLiteral(_) => Expr::NationalStringLiteral(converted),
1009                    _ => Expr::StringLiteral(converted),
1010                }
1011            } else {
1012                expr
1013            }
1014        }
1015        _ => transform_expr(expr, target),
1016    }
1017}
1018
1019/// Detect the format style from a format string based on its content.
1020fn detect_format_style(format_str: &str) -> time::TimeFormatStyle {
1021    // Check for style-specific patterns
1022    if format_str.contains('%') {
1023        // strftime-style format
1024        if format_str.contains("%i") {
1025            // MySQL uses %i for minutes
1026            time::TimeFormatStyle::Mysql
1027        } else {
1028            // Generic strftime (SQLite, BigQuery, etc.)
1029            time::TimeFormatStyle::Strftime
1030        }
1031    } else if format_str.contains("YYYY") || format_str.contains("yyyy") {
1032        // Check for Java vs Postgres/Snowflake
1033        if format_str.contains("HH24") || format_str.contains("MI") || format_str.contains("SS") {
1034            // Postgres/Oracle style
1035            time::TimeFormatStyle::Postgres
1036        } else if format_str.contains("mm") && format_str.contains("ss") {
1037            // Java style (lowercase seconds and minutes)
1038            time::TimeFormatStyle::Java
1039        } else if format_str.contains("FF") {
1040            // Snowflake fractional seconds
1041            time::TimeFormatStyle::Snowflake
1042        } else if format_str.contains("MM") && format_str.contains("DD") {
1043            // Could be Postgres or Snowflake - default to Postgres
1044            time::TimeFormatStyle::Postgres
1045        } else {
1046            // Default to Java for ambiguous cases with lowercase patterns
1047            time::TimeFormatStyle::Java
1048        }
1049    } else {
1050        // Unknown format - default to strftime
1051        time::TimeFormatStyle::Strftime
1052    }
1053}
1054
1055// ═══════════════════════════════════════════════════════════════════════════
1056// Function name mapping
1057// ═══════════════════════════════════════════════════════════════════════════
1058
1059/// Map function names between dialects.
1060pub(crate) fn map_function_name(name: &str, target: Dialect) -> String {
1061    let upper = name.to_uppercase();
1062    match upper.as_str() {
1063        // ── NOW / CURRENT_TIMESTAMP / GETDATE ────────────────────────────
1064        "NOW" => {
1065            if is_tsql_family(target) {
1066                "GETDATE".to_string()
1067            } else if matches!(
1068                target,
1069                Dialect::Ansi
1070                    | Dialect::BigQuery
1071                    | Dialect::Snowflake
1072                    | Dialect::Oracle
1073                    | Dialect::ClickHouse
1074                    | Dialect::Exasol
1075                    | Dialect::Teradata
1076                    | Dialect::Druid
1077                    | Dialect::Dremio
1078                    | Dialect::Tableau
1079            ) || is_presto_family(target)
1080                || is_hive_family(target)
1081            {
1082                "CURRENT_TIMESTAMP".to_string()
1083            } else {
1084                // Postgres, MySQL, SQLite, DuckDB, Redshift, etc. – keep NOW
1085                name.to_string()
1086            }
1087        }
1088        "GETDATE" => {
1089            if is_tsql_family(target) {
1090                name.to_string()
1091            } else if is_postgres_family(target)
1092                || matches!(target, Dialect::Mysql | Dialect::DuckDb | Dialect::Sqlite)
1093            {
1094                "NOW".to_string()
1095            } else {
1096                "CURRENT_TIMESTAMP".to_string()
1097            }
1098        }
1099
1100        // ── LEN / LENGTH ─────────────────────────────────────────────────
1101        "LEN" => {
1102            if is_tsql_family(target) || matches!(target, Dialect::BigQuery | Dialect::Snowflake) {
1103                name.to_string()
1104            } else {
1105                "LENGTH".to_string()
1106            }
1107        }
1108        "LENGTH" if is_tsql_family(target) => "LEN".to_string(),
1109
1110        // ── SUBSTR / SUBSTRING ───────────────────────────────────────────
1111        "SUBSTR" => {
1112            if is_mysql_family(target)
1113                || matches!(target, Dialect::Sqlite | Dialect::Oracle)
1114                || is_hive_family(target)
1115            {
1116                "SUBSTR".to_string()
1117            } else {
1118                "SUBSTRING".to_string()
1119            }
1120        }
1121        "SUBSTRING" => {
1122            if is_mysql_family(target)
1123                || matches!(target, Dialect::Sqlite | Dialect::Oracle)
1124                || is_hive_family(target)
1125            {
1126                "SUBSTR".to_string()
1127            } else {
1128                name.to_string()
1129            }
1130        }
1131
1132        // ── IFNULL / COALESCE / ISNULL ───────────────────────────────────
1133        "IFNULL" => {
1134            if is_tsql_family(target) {
1135                "ISNULL".to_string()
1136            } else if is_mysql_family(target) || matches!(target, Dialect::Sqlite) {
1137                // MySQL family + SQLite natively support IFNULL
1138                name.to_string()
1139            } else {
1140                "COALESCE".to_string()
1141            }
1142        }
1143        "ISNULL" => {
1144            if is_tsql_family(target) {
1145                name.to_string()
1146            } else if is_mysql_family(target) || matches!(target, Dialect::Sqlite) {
1147                "IFNULL".to_string()
1148            } else {
1149                "COALESCE".to_string()
1150            }
1151        }
1152
1153        // ── NVL → COALESCE (Oracle to others) ───────────────────────────
1154        "NVL" => {
1155            if matches!(target, Dialect::Oracle | Dialect::Snowflake) {
1156                name.to_string()
1157            } else if is_mysql_family(target) || matches!(target, Dialect::Sqlite) {
1158                "IFNULL".to_string()
1159            } else if is_tsql_family(target) {
1160                "ISNULL".to_string()
1161            } else {
1162                "COALESCE".to_string()
1163            }
1164        }
1165
1166        // ── RANDOM / RAND ────────────────────────────────────────────────
1167        "RANDOM" => {
1168            if matches!(
1169                target,
1170                Dialect::Postgres | Dialect::Sqlite | Dialect::DuckDb
1171            ) {
1172                name.to_string()
1173            } else {
1174                "RAND".to_string()
1175            }
1176        }
1177        "RAND" => {
1178            if matches!(
1179                target,
1180                Dialect::Postgres | Dialect::Sqlite | Dialect::DuckDb
1181            ) {
1182                "RANDOM".to_string()
1183            } else {
1184                name.to_string()
1185            }
1186        }
1187
1188        // ── POSITION / CHARINDEX ─────────────────────────────────────────
1189        "POSITION" if is_tsql_family(target) => "CHARINDEX".to_string(),
1190        "CHARINDEX" if is_postgres_family(target) => "POSITION".to_string(),
1191
1192        // Everything else – preserve original name
1193        _ => name.to_string(),
1194    }
1195}
1196
1197// ═══════════════════════════════════════════════════════════════════════════
1198// Data-type mapping
1199// ═══════════════════════════════════════════════════════════════════════════
1200
1201/// Map data types between dialects.
1202pub(crate) fn map_data_type(dt: DataType, target: Dialect) -> DataType {
1203    match (dt, target) {
1204        // ── T-SQL type mappings ─────────────────────────────────────────
1205        (DataType::Text, t) if is_tsql_family(t) => {
1206            // TEXT is a large-object string; emit VARCHAR(MAX) so the value is
1207            // not truncated to the MSSQL CAST default length of 30.
1208            DataType::VarcharMax
1209        }
1210        (DataType::Boolean, t) if is_tsql_family(t) => DataType::Bit(None),
1211        (DataType::Bytea, t) if is_tsql_family(t) => DataType::Varbinary(None),
1212        (DataType::Json, t) if is_tsql_family(t) => DataType::Varchar(None),
1213        (DataType::Jsonb, t) if is_tsql_family(t) => DataType::Varchar(None),
1214        (DataType::Uuid, t) if is_tsql_family(t) => {
1215            DataType::Unknown("UNIQUEIDENTIFIER".to_string())
1216        }
1217        (DataType::Serial, t) if is_tsql_family(t) => DataType::Int,
1218        (DataType::BigSerial, t) if is_tsql_family(t) => DataType::BigInt,
1219        (DataType::SmallSerial, t) if is_tsql_family(t) => DataType::SmallInt,
1220        (DataType::Timestamp { .. }, t) if is_tsql_family(t) => {
1221            DataType::Unknown("DATETIME2".to_string())
1222        }
1223        (DataType::Real, t) if is_tsql_family(t) => DataType::Real,
1224
1225        // ── TEXT / STRING ────────────────────────────────────────────────
1226        // TEXT → STRING for BigQuery, Hive, Spark, Databricks
1227        (DataType::Text, t) if matches!(t, Dialect::BigQuery) || is_hive_family(t) => {
1228            DataType::String
1229        }
1230        // STRING → TEXT for Postgres family, MySQL family, SQLite
1231        (DataType::String, t)
1232            if is_postgres_family(t) || is_mysql_family(t) || matches!(t, Dialect::Sqlite) =>
1233        {
1234            DataType::Text
1235        }
1236
1237        // ── INT → BIGINT (BigQuery) ─────────────────────────────────────
1238        (DataType::Int, Dialect::BigQuery) => DataType::BigInt,
1239
1240        // ── FLOAT → DOUBLE (BigQuery) ───────────────────────────────────
1241        (DataType::Float, Dialect::BigQuery) => DataType::Double,
1242
1243        // ── BYTEA ↔ BLOB ────────────────────────────────────────────────
1244        (DataType::Bytea, t)
1245            if is_mysql_family(t)
1246                || matches!(t, Dialect::Sqlite | Dialect::Oracle)
1247                || is_hive_family(t) =>
1248        {
1249            DataType::Blob
1250        }
1251        (DataType::Blob, t) if is_postgres_family(t) => DataType::Bytea,
1252        (DataType::Varbinary(_), t) if is_postgres_family(t) => DataType::Bytea,
1253
1254        // ── BOOLEAN → BOOL ──────────────────────────────────────────────
1255        (DataType::Boolean, Dialect::Mysql) => DataType::Boolean,
1256
1257        // Everything else is unchanged
1258        (dt, _) => dt,
1259    }
1260}
1261
1262// ═══════════════════════════════════════════════════════════════════════════
1263// LIMIT / TOP / FETCH FIRST transform
1264// ═══════════════════════════════════════════════════════════════════════════
1265
1266/// Transform LIMIT / TOP / FETCH FIRST between dialects.
1267///
1268/// - T-SQL family:  `LIMIT n` → `TOP n` (OFFSET + FETCH handled separately)
1269/// - Oracle:        `LIMIT n` → `FETCH FIRST n ROWS ONLY`
1270/// - All others:    `TOP n` / `FETCH FIRST n` → `LIMIT n`
1271fn transform_limit(sel: &mut SelectStatement, target: Dialect) {
1272    if is_tsql_family(target) {
1273        // Move LIMIT → TOP for T-SQL (only when there's no OFFSET)
1274        if let Some(limit) = sel.limit.take() {
1275            if sel.offset.is_none() {
1276                sel.top = Some(Box::new(limit));
1277            } else {
1278                // T-SQL with OFFSET uses OFFSET n ROWS FETCH NEXT m ROWS ONLY
1279                sel.fetch_first = Some(limit);
1280                // T-SQL OFFSET/FETCH requires ORDER BY. Add ORDER BY (SELECT NULL) if absent.
1281                if sel.order_by.is_empty() {
1282                    sel.order_by = vec![OrderByItem {
1283                        expr: Expr::Subquery(Box::new(Statement::Select(SelectStatement {
1284                            comments: Vec::new(),
1285                            ctes: Vec::new(),
1286                            distinct: false,
1287                            top: None,
1288                            columns: vec![SelectItem::Expr {
1289                                expr: Expr::Null,
1290                                alias: None,
1291                                alias_quote_style: QuoteStyle::None,
1292                            }],
1293                            from: None,
1294                            joins: Vec::new(),
1295                            where_clause: None,
1296                            group_by: Vec::new(),
1297                            having: None,
1298                            order_by: Vec::new(),
1299                            limit: None,
1300                            offset: None,
1301                            fetch_first: None,
1302                            qualify: None,
1303                            window_definitions: Vec::new(),
1304                        }))),
1305                        ascending: true,
1306                        nulls_first: None,
1307                    }];
1308                }
1309            }
1310        }
1311        // Also move fetch_first → top when no offset
1312        if sel.offset.is_none() {
1313            if let Some(fetch) = sel.fetch_first.take() {
1314                sel.top = Some(Box::new(fetch));
1315            }
1316        }
1317    } else if matches!(target, Dialect::Oracle) {
1318        // Oracle prefers FETCH FIRST n ROWS ONLY (SQL:2008 syntax)
1319        if let Some(limit) = sel.limit.take() {
1320            sel.fetch_first = Some(limit);
1321        }
1322        if let Some(top) = sel.top.take() {
1323            sel.fetch_first = Some(*top);
1324        }
1325    } else {
1326        // All other dialects: normalize to LIMIT
1327        if let Some(top) = sel.top.take() {
1328            if sel.limit.is_none() {
1329                sel.limit = Some(*top);
1330            }
1331        }
1332        if let Some(fetch) = sel.fetch_first.take() {
1333            if sel.limit.is_none() {
1334                sel.limit = Some(fetch);
1335            }
1336        }
1337    }
1338}
1339
1340// ═══════════════════════════════════════════════════════════════════════════
1341// Quoted-identifier transform
1342// ═══════════════════════════════════════════════════════════════════════════
1343
1344/// Convert any quoted identifiers in expressions to the target dialect's
1345/// quoting convention.
1346fn transform_quotes(expr: Expr, target: Dialect) -> Expr {
1347    match expr {
1348        Expr::Column {
1349            table,
1350            name,
1351            quote_style,
1352            table_quote_style,
1353        } => {
1354            let new_qs = if quote_style.is_quoted() {
1355                QuoteStyle::for_dialect(target)
1356            } else {
1357                QuoteStyle::None
1358            };
1359            let new_tqs = if table_quote_style.is_quoted() {
1360                QuoteStyle::for_dialect(target)
1361            } else {
1362                QuoteStyle::None
1363            };
1364            Expr::Column {
1365                table,
1366                name,
1367                quote_style: new_qs,
1368                table_quote_style: new_tqs,
1369            }
1370        }
1371        // Recurse into sub-expressions
1372        Expr::BinaryOp { left, op, right } => Expr::BinaryOp {
1373            left: Box::new(transform_quotes(*left, target)),
1374            op,
1375            right: Box::new(transform_quotes(*right, target)),
1376        },
1377        Expr::UnaryOp { op, expr } => Expr::UnaryOp {
1378            op,
1379            expr: Box::new(transform_quotes(*expr, target)),
1380        },
1381        Expr::Function {
1382            name,
1383            args,
1384            distinct,
1385            filter,
1386            over,
1387            order_by,
1388            within_group,
1389        } => Expr::Function {
1390            name,
1391            args: args
1392                .into_iter()
1393                .map(|a| transform_quotes(a, target))
1394                .collect(),
1395            distinct,
1396            filter: filter.map(|f| Box::new(transform_quotes(*f, target))),
1397            over,
1398            order_by,
1399            within_group,
1400        },
1401        Expr::TypedFunction { func, filter, over } => Expr::TypedFunction {
1402            func: func.transform_children(&|e| transform_quotes(e, target)),
1403            filter: filter.map(|f| Box::new(transform_quotes(*f, target))),
1404            over,
1405        },
1406        Expr::Nested(inner) => Expr::Nested(Box::new(transform_quotes(*inner, target))),
1407        Expr::Alias { expr, name } => Expr::Alias {
1408            expr: Box::new(transform_quotes(*expr, target)),
1409            name,
1410        },
1411        other => other,
1412    }
1413}
1414
1415/// Transform quoting for all identifier-bearing nodes inside a SELECT.
1416fn transform_quotes_in_select(sel: &mut SelectStatement, target: Dialect) {
1417    // Columns in the select list
1418    for item in &mut sel.columns {
1419        if let SelectItem::Expr { expr, .. } = item {
1420            *expr = transform_quotes(expr.clone(), target);
1421        }
1422    }
1423    // WHERE
1424    if let Some(wh) = &mut sel.where_clause {
1425        *wh = transform_quotes(wh.clone(), target);
1426    }
1427    // GROUP BY
1428    for gb in &mut sel.group_by {
1429        *gb = transform_quotes(gb.clone(), target);
1430    }
1431    // HAVING
1432    if let Some(having) = &mut sel.having {
1433        *having = transform_quotes(having.clone(), target);
1434    }
1435    // ORDER BY
1436    for ob in &mut sel.order_by {
1437        ob.expr = transform_quotes(ob.expr.clone(), target);
1438    }
1439    // Table refs (FROM, JOINs)
1440    if let Some(from) = &mut sel.from {
1441        transform_quotes_in_table_source(&mut from.source, target);
1442    }
1443    for join in &mut sel.joins {
1444        transform_quotes_in_table_source(&mut join.table, target);
1445        if let Some(on) = &mut join.on {
1446            *on = transform_quotes(on.clone(), target);
1447        }
1448    }
1449}
1450
1451fn transform_quotes_in_table_source(source: &mut TableSource, target: Dialect) {
1452    match source {
1453        TableSource::Table(tref) => {
1454            if tref.name_quote_style.is_quoted() {
1455                tref.name_quote_style = QuoteStyle::for_dialect(target);
1456            }
1457        }
1458        TableSource::Subquery { .. } => {}
1459        TableSource::TableFunction { .. } => {}
1460        TableSource::Lateral { source } => transform_quotes_in_table_source(source, target),
1461        TableSource::Pivot { source, .. } | TableSource::Unpivot { source, .. } => {
1462            transform_quotes_in_table_source(source, target);
1463        }
1464        TableSource::Unnest { .. } => {}
1465    }
1466}
1467
1468// ═══════════════════════════════════════════════════════════════════════════
1469// Concat operator transform (Change 3: || → CONCAT() for T-SQL)
1470// ═══════════════════════════════════════════════════════════════════════════
1471
1472/// Collect all operands from a chain of `||` (Concat) operations into a flat list.
1473fn collect_concat_args(expr: &Expr, args: &mut Vec<Expr>) {
1474    match expr {
1475        Expr::BinaryOp {
1476            left,
1477            op: BinaryOperator::Concat,
1478            right,
1479        } => {
1480            collect_concat_args(left, args);
1481            collect_concat_args(right, args);
1482        }
1483        other => args.push(other.clone()),
1484    }
1485}
1486
1487// ═══════════════════════════════════════════════════════════════════════════
1488// Interval arithmetic transform (Change 6: expr ± INTERVAL → DATEADD())
1489// ═══════════════════════════════════════════════════════════════════════════
1490
1491/// Try to transform `expr ± INTERVAL 'n unit'` into `DATEADD(unit, ±n, expr)` for T-SQL.
1492/// Returns `Some(transformed_expr)` if the right side is an interval, `None` otherwise.
1493fn try_transform_interval_arithmetic(
1494    left: &Expr,
1495    op: &BinaryOperator,
1496    right: &Expr,
1497) -> Option<Expr> {
1498    // Check right side is an interval
1499    if let Expr::Interval { value, unit } = right {
1500        if let Some((count, unit_name)) = parse_interval_value(value, unit) {
1501            let final_count = if matches!(op, BinaryOperator::Minus) {
1502                -count
1503            } else {
1504                count
1505            };
1506            return Some(Expr::Function {
1507                name: "DATEADD".to_string(),
1508                args: vec![
1509                    // Use a Column expr for the datepart keyword (unquoted identifier)
1510                    Expr::Column {
1511                        table: None,
1512                        name: unit_name,
1513                        quote_style: QuoteStyle::None,
1514                        table_quote_style: QuoteStyle::None,
1515                    },
1516                    Expr::Number(final_count.to_string()),
1517                    left.clone(),
1518                ],
1519                distinct: false,
1520                filter: None,
1521                over: None,
1522                order_by: vec![],
1523                within_group: false,
1524            });
1525        }
1526    }
1527
1528    // Check left side is an interval (less common: INTERVAL '7 days' + col)
1529    if let Expr::Interval { value, unit } = left {
1530        if matches!(op, BinaryOperator::Plus) {
1531            if let Some((count, unit_name)) = parse_interval_value(value, unit) {
1532                return Some(Expr::Function {
1533                    name: "DATEADD".to_string(),
1534                    args: vec![
1535                        Expr::Column {
1536                            table: None,
1537                            name: unit_name,
1538                            quote_style: QuoteStyle::None,
1539                            table_quote_style: QuoteStyle::None,
1540                        },
1541                        Expr::Number(count.to_string()),
1542                        right.clone(),
1543                    ],
1544                    distinct: false,
1545                    filter: None,
1546                    over: None,
1547                    order_by: vec![],
1548                    within_group: false,
1549                });
1550            }
1551        }
1552    }
1553
1554    None
1555}
1556
1557/// Parse an interval value expression and optional unit into (count, T-SQL datepart name).
1558fn parse_interval_value(value: &Expr, unit: &Option<DateTimeField>) -> Option<(i64, String)> {
1559    // Case 1: INTERVAL '7 days' (value is a string literal containing "7 days")
1560    if let Expr::StringLiteral(s) = value {
1561        let parts: Vec<&str> = s.trim().split_whitespace().collect();
1562        if parts.len() == 2 {
1563            let count: i64 = parts[0].parse().ok()?;
1564            let unit_name = normalize_interval_unit(parts[1])?;
1565            return Some((count, unit_name));
1566        }
1567        if parts.len() == 1 {
1568            // Just a number in the string, unit must come from the `unit` field
1569            let count: i64 = parts[0].parse().ok()?;
1570            if let Some(u) = unit {
1571                let unit_name = datetime_field_to_tsql(u)?;
1572                return Some((count, unit_name));
1573            }
1574        }
1575    }
1576
1577    // Case 2: INTERVAL 7 DAY (value is a number, unit is DateTimeField)
1578    if let Expr::Number(n) = value {
1579        let count: i64 = n.parse().ok()?;
1580        if let Some(u) = unit {
1581            let unit_name = datetime_field_to_tsql(u)?;
1582            return Some((count, unit_name));
1583        }
1584    }
1585
1586    None
1587}
1588
1589/// Normalize an interval unit string to a T-SQL DATEADD part name.
1590fn normalize_interval_unit(unit: &str) -> Option<String> {
1591    let lower = unit.to_lowercase();
1592    let normalized = lower.trim_end_matches('s');
1593    match normalized {
1594        "year" => Some("YEAR".to_string()),
1595        "month" => Some("MONTH".to_string()),
1596        "week" => Some("WEEK".to_string()),
1597        "day" => Some("DAY".to_string()),
1598        "hour" => Some("HOUR".to_string()),
1599        "minute" => Some("MINUTE".to_string()),
1600        "second" => Some("SECOND".to_string()),
1601        "millisecond" => Some("MILLISECOND".to_string()),
1602        "microsecond" => Some("MICROSECOND".to_string()),
1603        _ => None,
1604    }
1605}
1606
1607/// Convert a DateTimeField to T-SQL DATEADD unit name.
1608fn datetime_field_to_tsql(field: &DateTimeField) -> Option<String> {
1609    match field {
1610        DateTimeField::Year => Some("YEAR".to_string()),
1611        DateTimeField::Quarter => Some("QUARTER".to_string()),
1612        DateTimeField::Month => Some("MONTH".to_string()),
1613        DateTimeField::Week => Some("WEEK".to_string()),
1614        DateTimeField::Day => Some("DAY".to_string()),
1615        DateTimeField::Hour => Some("HOUR".to_string()),
1616        DateTimeField::Minute => Some("MINUTE".to_string()),
1617        DateTimeField::Second => Some("SECOND".to_string()),
1618        DateTimeField::Millisecond => Some("MILLISECOND".to_string()),
1619        DateTimeField::Microsecond => Some("MICROSECOND".to_string()),
1620        _ => None,
1621    }
1622}
1623
1624// ═══════════════════════════════════════════════════════════════════════════
1625// SIMILAR TO → LIKE pattern simplification (Change 9)
1626// ═══════════════════════════════════════════════════════════════════════════
1627
1628/// Simplify a SIMILAR TO pattern for use with LIKE.
1629/// Strips regex features (|, (), +, *) that T-SQL LIKE doesn't support.
1630fn simplify_similar_to_pattern(pattern: &Expr) -> Expr {
1631    if let Expr::StringLiteral(s) = pattern {
1632        let simplified = s.replace('|', "%").replace('(', "").replace(')', "");
1633        Expr::StringLiteral(simplified)
1634    } else {
1635        pattern.clone()
1636    }
1637}