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            DataType::Varchar(None) // NVARCHAR(MAX) emitted by generator via Unknown
1207        }
1208        (DataType::Boolean, t) if is_tsql_family(t) => DataType::Bit(None),
1209        (DataType::Bytea, t) if is_tsql_family(t) => DataType::Varbinary(None),
1210        (DataType::Json, t) if is_tsql_family(t) => DataType::Varchar(None),
1211        (DataType::Jsonb, t) if is_tsql_family(t) => DataType::Varchar(None),
1212        (DataType::Uuid, t) if is_tsql_family(t) => {
1213            DataType::Unknown("UNIQUEIDENTIFIER".to_string())
1214        }
1215        (DataType::Serial, t) if is_tsql_family(t) => DataType::Int,
1216        (DataType::BigSerial, t) if is_tsql_family(t) => DataType::BigInt,
1217        (DataType::SmallSerial, t) if is_tsql_family(t) => DataType::SmallInt,
1218        (DataType::Timestamp { .. }, t) if is_tsql_family(t) => {
1219            DataType::Unknown("DATETIME2".to_string())
1220        }
1221        (DataType::Real, t) if is_tsql_family(t) => DataType::Real,
1222
1223        // ── TEXT / STRING ────────────────────────────────────────────────
1224        // TEXT → STRING for BigQuery, Hive, Spark, Databricks
1225        (DataType::Text, t) if matches!(t, Dialect::BigQuery) || is_hive_family(t) => {
1226            DataType::String
1227        }
1228        // STRING → TEXT for Postgres family, MySQL family, SQLite
1229        (DataType::String, t)
1230            if is_postgres_family(t) || is_mysql_family(t) || matches!(t, Dialect::Sqlite) =>
1231        {
1232            DataType::Text
1233        }
1234
1235        // ── INT → BIGINT (BigQuery) ─────────────────────────────────────
1236        (DataType::Int, Dialect::BigQuery) => DataType::BigInt,
1237
1238        // ── FLOAT → DOUBLE (BigQuery) ───────────────────────────────────
1239        (DataType::Float, Dialect::BigQuery) => DataType::Double,
1240
1241        // ── BYTEA ↔ BLOB ────────────────────────────────────────────────
1242        (DataType::Bytea, t)
1243            if is_mysql_family(t)
1244                || matches!(t, Dialect::Sqlite | Dialect::Oracle)
1245                || is_hive_family(t) =>
1246        {
1247            DataType::Blob
1248        }
1249        (DataType::Blob, t) if is_postgres_family(t) => DataType::Bytea,
1250        (DataType::Varbinary(_), t) if is_postgres_family(t) => DataType::Bytea,
1251
1252        // ── BOOLEAN → BOOL ──────────────────────────────────────────────
1253        (DataType::Boolean, Dialect::Mysql) => DataType::Boolean,
1254
1255        // Everything else is unchanged
1256        (dt, _) => dt,
1257    }
1258}
1259
1260// ═══════════════════════════════════════════════════════════════════════════
1261// LIMIT / TOP / FETCH FIRST transform
1262// ═══════════════════════════════════════════════════════════════════════════
1263
1264/// Transform LIMIT / TOP / FETCH FIRST between dialects.
1265///
1266/// - T-SQL family:  `LIMIT n` → `TOP n` (OFFSET + FETCH handled separately)
1267/// - Oracle:        `LIMIT n` → `FETCH FIRST n ROWS ONLY`
1268/// - All others:    `TOP n` / `FETCH FIRST n` → `LIMIT n`
1269fn transform_limit(sel: &mut SelectStatement, target: Dialect) {
1270    if is_tsql_family(target) {
1271        // Move LIMIT → TOP for T-SQL (only when there's no OFFSET)
1272        if let Some(limit) = sel.limit.take() {
1273            if sel.offset.is_none() {
1274                sel.top = Some(Box::new(limit));
1275            } else {
1276                // T-SQL with OFFSET uses OFFSET n ROWS FETCH NEXT m ROWS ONLY
1277                sel.fetch_first = Some(limit);
1278                // T-SQL OFFSET/FETCH requires ORDER BY. Add ORDER BY (SELECT NULL) if absent.
1279                if sel.order_by.is_empty() {
1280                    sel.order_by = vec![OrderByItem {
1281                        expr: Expr::Subquery(Box::new(Statement::Select(SelectStatement {
1282                            comments: Vec::new(),
1283                            ctes: Vec::new(),
1284                            distinct: false,
1285                            top: None,
1286                            columns: vec![SelectItem::Expr {
1287                                expr: Expr::Null,
1288                                alias: None,
1289                                alias_quote_style: QuoteStyle::None,
1290                            }],
1291                            from: None,
1292                            joins: Vec::new(),
1293                            where_clause: None,
1294                            group_by: Vec::new(),
1295                            having: None,
1296                            order_by: Vec::new(),
1297                            limit: None,
1298                            offset: None,
1299                            fetch_first: None,
1300                            qualify: None,
1301                            window_definitions: Vec::new(),
1302                        }))),
1303                        ascending: true,
1304                        nulls_first: None,
1305                    }];
1306                }
1307            }
1308        }
1309        // Also move fetch_first → top when no offset
1310        if sel.offset.is_none() {
1311            if let Some(fetch) = sel.fetch_first.take() {
1312                sel.top = Some(Box::new(fetch));
1313            }
1314        }
1315    } else if matches!(target, Dialect::Oracle) {
1316        // Oracle prefers FETCH FIRST n ROWS ONLY (SQL:2008 syntax)
1317        if let Some(limit) = sel.limit.take() {
1318            sel.fetch_first = Some(limit);
1319        }
1320        if let Some(top) = sel.top.take() {
1321            sel.fetch_first = Some(*top);
1322        }
1323    } else {
1324        // All other dialects: normalize to LIMIT
1325        if let Some(top) = sel.top.take() {
1326            if sel.limit.is_none() {
1327                sel.limit = Some(*top);
1328            }
1329        }
1330        if let Some(fetch) = sel.fetch_first.take() {
1331            if sel.limit.is_none() {
1332                sel.limit = Some(fetch);
1333            }
1334        }
1335    }
1336}
1337
1338// ═══════════════════════════════════════════════════════════════════════════
1339// Quoted-identifier transform
1340// ═══════════════════════════════════════════════════════════════════════════
1341
1342/// Convert any quoted identifiers in expressions to the target dialect's
1343/// quoting convention.
1344fn transform_quotes(expr: Expr, target: Dialect) -> Expr {
1345    match expr {
1346        Expr::Column {
1347            table,
1348            name,
1349            quote_style,
1350            table_quote_style,
1351        } => {
1352            let new_qs = if quote_style.is_quoted() {
1353                QuoteStyle::for_dialect(target)
1354            } else {
1355                QuoteStyle::None
1356            };
1357            let new_tqs = if table_quote_style.is_quoted() {
1358                QuoteStyle::for_dialect(target)
1359            } else {
1360                QuoteStyle::None
1361            };
1362            Expr::Column {
1363                table,
1364                name,
1365                quote_style: new_qs,
1366                table_quote_style: new_tqs,
1367            }
1368        }
1369        // Recurse into sub-expressions
1370        Expr::BinaryOp { left, op, right } => Expr::BinaryOp {
1371            left: Box::new(transform_quotes(*left, target)),
1372            op,
1373            right: Box::new(transform_quotes(*right, target)),
1374        },
1375        Expr::UnaryOp { op, expr } => Expr::UnaryOp {
1376            op,
1377            expr: Box::new(transform_quotes(*expr, target)),
1378        },
1379        Expr::Function {
1380            name,
1381            args,
1382            distinct,
1383            filter,
1384            over,
1385            order_by,
1386            within_group,
1387        } => Expr::Function {
1388            name,
1389            args: args
1390                .into_iter()
1391                .map(|a| transform_quotes(a, target))
1392                .collect(),
1393            distinct,
1394            filter: filter.map(|f| Box::new(transform_quotes(*f, target))),
1395            over,
1396            order_by,
1397            within_group,
1398        },
1399        Expr::TypedFunction { func, filter, over } => Expr::TypedFunction {
1400            func: func.transform_children(&|e| transform_quotes(e, target)),
1401            filter: filter.map(|f| Box::new(transform_quotes(*f, target))),
1402            over,
1403        },
1404        Expr::Nested(inner) => Expr::Nested(Box::new(transform_quotes(*inner, target))),
1405        Expr::Alias { expr, name } => Expr::Alias {
1406            expr: Box::new(transform_quotes(*expr, target)),
1407            name,
1408        },
1409        other => other,
1410    }
1411}
1412
1413/// Transform quoting for all identifier-bearing nodes inside a SELECT.
1414fn transform_quotes_in_select(sel: &mut SelectStatement, target: Dialect) {
1415    // Columns in the select list
1416    for item in &mut sel.columns {
1417        if let SelectItem::Expr { expr, .. } = item {
1418            *expr = transform_quotes(expr.clone(), target);
1419        }
1420    }
1421    // WHERE
1422    if let Some(wh) = &mut sel.where_clause {
1423        *wh = transform_quotes(wh.clone(), target);
1424    }
1425    // GROUP BY
1426    for gb in &mut sel.group_by {
1427        *gb = transform_quotes(gb.clone(), target);
1428    }
1429    // HAVING
1430    if let Some(having) = &mut sel.having {
1431        *having = transform_quotes(having.clone(), target);
1432    }
1433    // ORDER BY
1434    for ob in &mut sel.order_by {
1435        ob.expr = transform_quotes(ob.expr.clone(), target);
1436    }
1437    // Table refs (FROM, JOINs)
1438    if let Some(from) = &mut sel.from {
1439        transform_quotes_in_table_source(&mut from.source, target);
1440    }
1441    for join in &mut sel.joins {
1442        transform_quotes_in_table_source(&mut join.table, target);
1443        if let Some(on) = &mut join.on {
1444            *on = transform_quotes(on.clone(), target);
1445        }
1446    }
1447}
1448
1449fn transform_quotes_in_table_source(source: &mut TableSource, target: Dialect) {
1450    match source {
1451        TableSource::Table(tref) => {
1452            if tref.name_quote_style.is_quoted() {
1453                tref.name_quote_style = QuoteStyle::for_dialect(target);
1454            }
1455        }
1456        TableSource::Subquery { .. } => {}
1457        TableSource::TableFunction { .. } => {}
1458        TableSource::Lateral { source } => transform_quotes_in_table_source(source, target),
1459        TableSource::Pivot { source, .. } | TableSource::Unpivot { source, .. } => {
1460            transform_quotes_in_table_source(source, target);
1461        }
1462        TableSource::Unnest { .. } => {}
1463    }
1464}
1465
1466// ═══════════════════════════════════════════════════════════════════════════
1467// Concat operator transform (Change 3: || → CONCAT() for T-SQL)
1468// ═══════════════════════════════════════════════════════════════════════════
1469
1470/// Collect all operands from a chain of `||` (Concat) operations into a flat list.
1471fn collect_concat_args(expr: &Expr, args: &mut Vec<Expr>) {
1472    match expr {
1473        Expr::BinaryOp {
1474            left,
1475            op: BinaryOperator::Concat,
1476            right,
1477        } => {
1478            collect_concat_args(left, args);
1479            collect_concat_args(right, args);
1480        }
1481        other => args.push(other.clone()),
1482    }
1483}
1484
1485// ═══════════════════════════════════════════════════════════════════════════
1486// Interval arithmetic transform (Change 6: expr ± INTERVAL → DATEADD())
1487// ═══════════════════════════════════════════════════════════════════════════
1488
1489/// Try to transform `expr ± INTERVAL 'n unit'` into `DATEADD(unit, ±n, expr)` for T-SQL.
1490/// Returns `Some(transformed_expr)` if the right side is an interval, `None` otherwise.
1491fn try_transform_interval_arithmetic(
1492    left: &Expr,
1493    op: &BinaryOperator,
1494    right: &Expr,
1495) -> Option<Expr> {
1496    // Check right side is an interval
1497    if let Expr::Interval { value, unit } = right {
1498        if let Some((count, unit_name)) = parse_interval_value(value, unit) {
1499            let final_count = if matches!(op, BinaryOperator::Minus) {
1500                -count
1501            } else {
1502                count
1503            };
1504            return Some(Expr::Function {
1505                name: "DATEADD".to_string(),
1506                args: vec![
1507                    // Use a Column expr for the datepart keyword (unquoted identifier)
1508                    Expr::Column {
1509                        table: None,
1510                        name: unit_name,
1511                        quote_style: QuoteStyle::None,
1512                        table_quote_style: QuoteStyle::None,
1513                    },
1514                    Expr::Number(final_count.to_string()),
1515                    left.clone(),
1516                ],
1517                distinct: false,
1518                filter: None,
1519                over: None,
1520                order_by: vec![],
1521                within_group: false,
1522            });
1523        }
1524    }
1525
1526    // Check left side is an interval (less common: INTERVAL '7 days' + col)
1527    if let Expr::Interval { value, unit } = left {
1528        if matches!(op, BinaryOperator::Plus) {
1529            if let Some((count, unit_name)) = parse_interval_value(value, unit) {
1530                return Some(Expr::Function {
1531                    name: "DATEADD".to_string(),
1532                    args: vec![
1533                        Expr::Column {
1534                            table: None,
1535                            name: unit_name,
1536                            quote_style: QuoteStyle::None,
1537                            table_quote_style: QuoteStyle::None,
1538                        },
1539                        Expr::Number(count.to_string()),
1540                        right.clone(),
1541                    ],
1542                    distinct: false,
1543                    filter: None,
1544                    over: None,
1545                    order_by: vec![],
1546                    within_group: false,
1547                });
1548            }
1549        }
1550    }
1551
1552    None
1553}
1554
1555/// Parse an interval value expression and optional unit into (count, T-SQL datepart name).
1556fn parse_interval_value(value: &Expr, unit: &Option<DateTimeField>) -> Option<(i64, String)> {
1557    // Case 1: INTERVAL '7 days' (value is a string literal containing "7 days")
1558    if let Expr::StringLiteral(s) = value {
1559        let parts: Vec<&str> = s.trim().split_whitespace().collect();
1560        if parts.len() == 2 {
1561            let count: i64 = parts[0].parse().ok()?;
1562            let unit_name = normalize_interval_unit(parts[1])?;
1563            return Some((count, unit_name));
1564        }
1565        if parts.len() == 1 {
1566            // Just a number in the string, unit must come from the `unit` field
1567            let count: i64 = parts[0].parse().ok()?;
1568            if let Some(u) = unit {
1569                let unit_name = datetime_field_to_tsql(u)?;
1570                return Some((count, unit_name));
1571            }
1572        }
1573    }
1574
1575    // Case 2: INTERVAL 7 DAY (value is a number, unit is DateTimeField)
1576    if let Expr::Number(n) = value {
1577        let count: i64 = n.parse().ok()?;
1578        if let Some(u) = unit {
1579            let unit_name = datetime_field_to_tsql(u)?;
1580            return Some((count, unit_name));
1581        }
1582    }
1583
1584    None
1585}
1586
1587/// Normalize an interval unit string to a T-SQL DATEADD part name.
1588fn normalize_interval_unit(unit: &str) -> Option<String> {
1589    let lower = unit.to_lowercase();
1590    let normalized = lower.trim_end_matches('s');
1591    match normalized {
1592        "year" => Some("YEAR".to_string()),
1593        "month" => Some("MONTH".to_string()),
1594        "week" => Some("WEEK".to_string()),
1595        "day" => Some("DAY".to_string()),
1596        "hour" => Some("HOUR".to_string()),
1597        "minute" => Some("MINUTE".to_string()),
1598        "second" => Some("SECOND".to_string()),
1599        "millisecond" => Some("MILLISECOND".to_string()),
1600        "microsecond" => Some("MICROSECOND".to_string()),
1601        _ => None,
1602    }
1603}
1604
1605/// Convert a DateTimeField to T-SQL DATEADD unit name.
1606fn datetime_field_to_tsql(field: &DateTimeField) -> Option<String> {
1607    match field {
1608        DateTimeField::Year => Some("YEAR".to_string()),
1609        DateTimeField::Quarter => Some("QUARTER".to_string()),
1610        DateTimeField::Month => Some("MONTH".to_string()),
1611        DateTimeField::Week => Some("WEEK".to_string()),
1612        DateTimeField::Day => Some("DAY".to_string()),
1613        DateTimeField::Hour => Some("HOUR".to_string()),
1614        DateTimeField::Minute => Some("MINUTE".to_string()),
1615        DateTimeField::Second => Some("SECOND".to_string()),
1616        DateTimeField::Millisecond => Some("MILLISECOND".to_string()),
1617        DateTimeField::Microsecond => Some("MICROSECOND".to_string()),
1618        _ => None,
1619    }
1620}
1621
1622// ═══════════════════════════════════════════════════════════════════════════
1623// SIMILAR TO → LIKE pattern simplification (Change 9)
1624// ═══════════════════════════════════════════════════════════════════════════
1625
1626/// Simplify a SIMILAR TO pattern for use with LIKE.
1627/// Strips regex features (|, (), +, *) that T-SQL LIKE doesn't support.
1628fn simplify_similar_to_pattern(pattern: &Expr) -> Expr {
1629    if let Expr::StringLiteral(s) = pattern {
1630        let simplified = s.replace('|', "%").replace('(', "").replace(')', "");
1631        Expr::StringLiteral(simplified)
1632    } else {
1633        pattern.clone()
1634    }
1635}