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/// Transform an expression for the target dialect.
756fn transform_expr(expr: Expr, target: Dialect) -> Expr {
757    match expr {
758        // Map function names across dialects
759        Expr::Function {
760            name,
761            args,
762            distinct,
763            filter,
764            over,
765            order_by,
766            within_group,
767        } => {
768            let new_name = map_function_name(&name, target);
769            let new_args: Vec<Expr> = args
770                .into_iter()
771                .map(|a| transform_expr(a, target))
772                .collect();
773            Expr::Function {
774                name: new_name,
775                args: new_args,
776                distinct,
777                filter: filter.map(|f| Box::new(transform_expr(*f, target))),
778                over,
779                order_by,
780                within_group,
781            }
782        }
783        // Recurse into typed function child expressions, with special handling
784        // for date/time formatting functions that need format string conversion
785        Expr::TypedFunction { func, filter, over } => {
786            let transformed_func = transform_typed_function(func, target);
787            Expr::TypedFunction {
788                func: transformed_func,
789                filter: filter.map(|f| Box::new(transform_expr(*f, target))),
790                over,
791            }
792        }
793        // ILIKE → LOWER(expr) LIKE LOWER(pattern) for non-supporting dialects
794        Expr::ILike {
795            expr,
796            pattern,
797            negated,
798            escape,
799        } if !supports_ilike_builtin(target) => Expr::Like {
800            expr: Box::new(Expr::TypedFunction {
801                func: TypedFunction::Lower {
802                    expr: Box::new(transform_expr(*expr, target)),
803                },
804                filter: None,
805                over: None,
806            }),
807            pattern: Box::new(Expr::TypedFunction {
808                func: TypedFunction::Lower {
809                    expr: Box::new(transform_expr(*pattern, target)),
810                },
811                filter: None,
812                over: None,
813            }),
814            negated,
815            escape,
816        },
817        // SIMILAR TO → LIKE for T-SQL (lossy: regex features dropped)
818        Expr::SimilarTo {
819            expr,
820            pattern,
821            negated,
822            escape,
823        } if is_tsql_family(target) => {
824            let transformed_pattern = transform_expr(*pattern, target);
825            let simplified = simplify_similar_to_pattern(&transformed_pattern);
826            Expr::Like {
827                expr: Box::new(transform_expr(*expr, target)),
828                pattern: Box::new(simplified),
829                negated,
830                escape,
831            }
832        }
833        // Map data types in CAST
834        Expr::Cast { expr, data_type } => Expr::Cast {
835            expr: Box::new(transform_expr(*expr, target)),
836            data_type: map_data_type(data_type, target),
837        },
838        // Recurse into binary ops, with T-SQL specific transforms
839        Expr::BinaryOp { left, op, right } => {
840            // Change 3: || → CONCAT() for T-SQL
841            // Collect args BEFORE recursive transform to flatten the full chain
842            if op == BinaryOperator::Concat && is_tsql_family(target) {
843                let mut args = Vec::new();
844                collect_concat_args(
845                    &Expr::BinaryOp {
846                        left,
847                        op: BinaryOperator::Concat,
848                        right,
849                    },
850                    &mut args,
851                );
852                // Now transform each collected arg
853                let args = args
854                    .into_iter()
855                    .map(|a| transform_expr(a, target))
856                    .collect();
857                return Expr::Function {
858                    name: "CONCAT".to_string(),
859                    args,
860                    distinct: false,
861                    filter: None,
862                    over: None,
863                    order_by: vec![],
864                    within_group: false,
865                };
866            }
867
868            let left_transformed = transform_expr(*left, target);
869            let right_transformed = transform_expr(*right, target);
870
871            // Change 6: expr ± INTERVAL → DATEADD() for T-SQL
872            if is_tsql_family(target) && matches!(op, BinaryOperator::Plus | BinaryOperator::Minus)
873            {
874                if let Some(dateadd) =
875                    try_transform_interval_arithmetic(&left_transformed, &op, &right_transformed)
876                {
877                    return dateadd;
878                }
879            }
880
881            Expr::BinaryOp {
882                left: Box::new(left_transformed),
883                op,
884                right: Box::new(right_transformed),
885            }
886        }
887        Expr::UnaryOp { op, expr } => Expr::UnaryOp {
888            op,
889            expr: Box::new(transform_expr(*expr, target)),
890        },
891        Expr::Nested(inner) => Expr::Nested(Box::new(transform_expr(*inner, target))),
892        // Transform quoting on column references
893        Expr::Column {
894            table,
895            name,
896            quote_style,
897            table_quote_style,
898        } => {
899            let new_qs = if quote_style.is_quoted() {
900                QuoteStyle::for_dialect(target)
901            } else {
902                QuoteStyle::None
903            };
904            let new_tqs = if table_quote_style.is_quoted() {
905                QuoteStyle::for_dialect(target)
906            } else {
907                QuoteStyle::None
908            };
909            Expr::Column {
910                table,
911                name,
912                quote_style: new_qs,
913                table_quote_style: new_tqs,
914            }
915        }
916        // Everything else stays the same
917        other => other,
918    }
919}
920
921// ═══════════════════════════════════════════════════════════════════════════
922// Typed function transformation with format string conversion
923// ═══════════════════════════════════════════════════════════════════════════
924
925/// Transform a TypedFunction, including date/time format string conversion.
926///
927/// For TimeToStr and StrToTime functions, this converts the format string
928/// from the source dialect's convention to the target dialect's convention.
929fn transform_typed_function(func: TypedFunction, target: Dialect) -> TypedFunction {
930    match func {
931        TypedFunction::TimeToStr { expr, format } => {
932            let transformed_expr = Box::new(transform_expr(*expr, target));
933            let transformed_format = transform_format_expr(*format, target);
934            TypedFunction::TimeToStr {
935                expr: transformed_expr,
936                format: Box::new(transformed_format),
937            }
938        }
939        TypedFunction::StrToTime { expr, format } => {
940            let transformed_expr = Box::new(transform_expr(*expr, target));
941            let transformed_format = transform_format_expr(*format, target);
942            TypedFunction::StrToTime {
943                expr: transformed_expr,
944                format: Box::new(transformed_format),
945            }
946        }
947        // For all other typed functions, just transform child expressions
948        other => other.transform_children(&|e| transform_expr(e, target)),
949    }
950}
951
952/// Transform a format string expression for the target dialect.
953///
954/// If the expression is a string literal, convert the format specifiers.
955/// Otherwise, just recursively transform child expressions.
956fn transform_format_expr(expr: Expr, target: Dialect) -> Expr {
957    // We need to know the source dialect to convert properly.
958    // Since we don't have access to the source dialect here, we use heuristics
959    // to detect the format style based on the format string content.
960    match &expr {
961        Expr::StringLiteral(s) | Expr::NationalStringLiteral(s) => {
962            let detected_source = detect_format_style(s);
963            let target_style = time::TimeFormatStyle::for_dialect(target);
964
965            // Only convert if styles differ
966            if detected_source != target_style {
967                let converted = time::format_time(s, detected_source, target_style);
968                match expr {
969                    Expr::NationalStringLiteral(_) => Expr::NationalStringLiteral(converted),
970                    _ => Expr::StringLiteral(converted),
971                }
972            } else {
973                expr
974            }
975        }
976        _ => transform_expr(expr, target),
977    }
978}
979
980/// Detect the format style from a format string based on its content.
981fn detect_format_style(format_str: &str) -> time::TimeFormatStyle {
982    // Check for style-specific patterns
983    if format_str.contains('%') {
984        // strftime-style format
985        if format_str.contains("%i") {
986            // MySQL uses %i for minutes
987            time::TimeFormatStyle::Mysql
988        } else {
989            // Generic strftime (SQLite, BigQuery, etc.)
990            time::TimeFormatStyle::Strftime
991        }
992    } else if format_str.contains("YYYY") || format_str.contains("yyyy") {
993        // Check for Java vs Postgres/Snowflake
994        if format_str.contains("HH24") || format_str.contains("MI") || format_str.contains("SS") {
995            // Postgres/Oracle style
996            time::TimeFormatStyle::Postgres
997        } else if format_str.contains("mm") && format_str.contains("ss") {
998            // Java style (lowercase seconds and minutes)
999            time::TimeFormatStyle::Java
1000        } else if format_str.contains("FF") {
1001            // Snowflake fractional seconds
1002            time::TimeFormatStyle::Snowflake
1003        } else if format_str.contains("MM") && format_str.contains("DD") {
1004            // Could be Postgres or Snowflake - default to Postgres
1005            time::TimeFormatStyle::Postgres
1006        } else {
1007            // Default to Java for ambiguous cases with lowercase patterns
1008            time::TimeFormatStyle::Java
1009        }
1010    } else {
1011        // Unknown format - default to strftime
1012        time::TimeFormatStyle::Strftime
1013    }
1014}
1015
1016// ═══════════════════════════════════════════════════════════════════════════
1017// Function name mapping
1018// ═══════════════════════════════════════════════════════════════════════════
1019
1020/// Map function names between dialects.
1021pub(crate) fn map_function_name(name: &str, target: Dialect) -> String {
1022    let upper = name.to_uppercase();
1023    match upper.as_str() {
1024        // ── NOW / CURRENT_TIMESTAMP / GETDATE ────────────────────────────
1025        "NOW" => {
1026            if is_tsql_family(target) {
1027                "GETDATE".to_string()
1028            } else if matches!(
1029                target,
1030                Dialect::Ansi
1031                    | Dialect::BigQuery
1032                    | Dialect::Snowflake
1033                    | Dialect::Oracle
1034                    | Dialect::ClickHouse
1035                    | Dialect::Exasol
1036                    | Dialect::Teradata
1037                    | Dialect::Druid
1038                    | Dialect::Dremio
1039                    | Dialect::Tableau
1040            ) || is_presto_family(target)
1041                || is_hive_family(target)
1042            {
1043                "CURRENT_TIMESTAMP".to_string()
1044            } else {
1045                // Postgres, MySQL, SQLite, DuckDB, Redshift, etc. – keep NOW
1046                name.to_string()
1047            }
1048        }
1049        "GETDATE" => {
1050            if is_tsql_family(target) {
1051                name.to_string()
1052            } else if is_postgres_family(target)
1053                || matches!(target, Dialect::Mysql | Dialect::DuckDb | Dialect::Sqlite)
1054            {
1055                "NOW".to_string()
1056            } else {
1057                "CURRENT_TIMESTAMP".to_string()
1058            }
1059        }
1060
1061        // ── LEN / LENGTH ─────────────────────────────────────────────────
1062        "LEN" => {
1063            if is_tsql_family(target) || matches!(target, Dialect::BigQuery | Dialect::Snowflake) {
1064                name.to_string()
1065            } else {
1066                "LENGTH".to_string()
1067            }
1068        }
1069        "LENGTH" if is_tsql_family(target) => "LEN".to_string(),
1070
1071        // ── SUBSTR / SUBSTRING ───────────────────────────────────────────
1072        "SUBSTR" => {
1073            if is_mysql_family(target)
1074                || matches!(target, Dialect::Sqlite | Dialect::Oracle)
1075                || is_hive_family(target)
1076            {
1077                "SUBSTR".to_string()
1078            } else {
1079                "SUBSTRING".to_string()
1080            }
1081        }
1082        "SUBSTRING" => {
1083            if is_mysql_family(target)
1084                || matches!(target, Dialect::Sqlite | Dialect::Oracle)
1085                || is_hive_family(target)
1086            {
1087                "SUBSTR".to_string()
1088            } else {
1089                name.to_string()
1090            }
1091        }
1092
1093        // ── IFNULL / COALESCE / ISNULL ───────────────────────────────────
1094        "IFNULL" => {
1095            if is_tsql_family(target) {
1096                "ISNULL".to_string()
1097            } else if is_mysql_family(target) || matches!(target, Dialect::Sqlite) {
1098                // MySQL family + SQLite natively support IFNULL
1099                name.to_string()
1100            } else {
1101                "COALESCE".to_string()
1102            }
1103        }
1104        "ISNULL" => {
1105            if is_tsql_family(target) {
1106                name.to_string()
1107            } else if is_mysql_family(target) || matches!(target, Dialect::Sqlite) {
1108                "IFNULL".to_string()
1109            } else {
1110                "COALESCE".to_string()
1111            }
1112        }
1113
1114        // ── NVL → COALESCE (Oracle to others) ───────────────────────────
1115        "NVL" => {
1116            if matches!(target, Dialect::Oracle | Dialect::Snowflake) {
1117                name.to_string()
1118            } else if is_mysql_family(target) || matches!(target, Dialect::Sqlite) {
1119                "IFNULL".to_string()
1120            } else if is_tsql_family(target) {
1121                "ISNULL".to_string()
1122            } else {
1123                "COALESCE".to_string()
1124            }
1125        }
1126
1127        // ── RANDOM / RAND ────────────────────────────────────────────────
1128        "RANDOM" => {
1129            if matches!(
1130                target,
1131                Dialect::Postgres | Dialect::Sqlite | Dialect::DuckDb
1132            ) {
1133                name.to_string()
1134            } else {
1135                "RAND".to_string()
1136            }
1137        }
1138        "RAND" => {
1139            if matches!(
1140                target,
1141                Dialect::Postgres | Dialect::Sqlite | Dialect::DuckDb
1142            ) {
1143                "RANDOM".to_string()
1144            } else {
1145                name.to_string()
1146            }
1147        }
1148
1149        // ── POSITION / CHARINDEX ─────────────────────────────────────────
1150        "POSITION" if is_tsql_family(target) => "CHARINDEX".to_string(),
1151        "CHARINDEX" if is_postgres_family(target) => "POSITION".to_string(),
1152
1153        // Everything else – preserve original name
1154        _ => name.to_string(),
1155    }
1156}
1157
1158// ═══════════════════════════════════════════════════════════════════════════
1159// Data-type mapping
1160// ═══════════════════════════════════════════════════════════════════════════
1161
1162/// Map data types between dialects.
1163pub(crate) fn map_data_type(dt: DataType, target: Dialect) -> DataType {
1164    match (dt, target) {
1165        // ── T-SQL type mappings ─────────────────────────────────────────
1166        (DataType::Text, t) if is_tsql_family(t) => {
1167            DataType::Varchar(None) // NVARCHAR(MAX) emitted by generator via Unknown
1168        }
1169        (DataType::Boolean, t) if is_tsql_family(t) => DataType::Bit(None),
1170        (DataType::Bytea, t) if is_tsql_family(t) => DataType::Varbinary(None),
1171        (DataType::Json, t) if is_tsql_family(t) => DataType::Varchar(None),
1172        (DataType::Jsonb, t) if is_tsql_family(t) => DataType::Varchar(None),
1173        (DataType::Uuid, t) if is_tsql_family(t) => {
1174            DataType::Unknown("UNIQUEIDENTIFIER".to_string())
1175        }
1176        (DataType::Serial, t) if is_tsql_family(t) => DataType::Int,
1177        (DataType::BigSerial, t) if is_tsql_family(t) => DataType::BigInt,
1178        (DataType::SmallSerial, t) if is_tsql_family(t) => DataType::SmallInt,
1179        (DataType::Timestamp { .. }, t) if is_tsql_family(t) => {
1180            DataType::Unknown("DATETIME2".to_string())
1181        }
1182        (DataType::Real, t) if is_tsql_family(t) => DataType::Real,
1183
1184        // ── TEXT / STRING ────────────────────────────────────────────────
1185        // TEXT → STRING for BigQuery, Hive, Spark, Databricks
1186        (DataType::Text, t) if matches!(t, Dialect::BigQuery) || is_hive_family(t) => {
1187            DataType::String
1188        }
1189        // STRING → TEXT for Postgres family, MySQL family, SQLite
1190        (DataType::String, t)
1191            if is_postgres_family(t) || is_mysql_family(t) || matches!(t, Dialect::Sqlite) =>
1192        {
1193            DataType::Text
1194        }
1195
1196        // ── INT → BIGINT (BigQuery) ─────────────────────────────────────
1197        (DataType::Int, Dialect::BigQuery) => DataType::BigInt,
1198
1199        // ── FLOAT → DOUBLE (BigQuery) ───────────────────────────────────
1200        (DataType::Float, Dialect::BigQuery) => DataType::Double,
1201
1202        // ── BYTEA ↔ BLOB ────────────────────────────────────────────────
1203        (DataType::Bytea, t)
1204            if is_mysql_family(t)
1205                || matches!(t, Dialect::Sqlite | Dialect::Oracle)
1206                || is_hive_family(t) =>
1207        {
1208            DataType::Blob
1209        }
1210        (DataType::Blob, t) if is_postgres_family(t) => DataType::Bytea,
1211        (DataType::Varbinary(_), t) if is_postgres_family(t) => DataType::Bytea,
1212
1213        // ── BOOLEAN → BOOL ──────────────────────────────────────────────
1214        (DataType::Boolean, Dialect::Mysql) => DataType::Boolean,
1215
1216        // Everything else is unchanged
1217        (dt, _) => dt,
1218    }
1219}
1220
1221// ═══════════════════════════════════════════════════════════════════════════
1222// LIMIT / TOP / FETCH FIRST transform
1223// ═══════════════════════════════════════════════════════════════════════════
1224
1225/// Transform LIMIT / TOP / FETCH FIRST between dialects.
1226///
1227/// - T-SQL family:  `LIMIT n` → `TOP n` (OFFSET + FETCH handled separately)
1228/// - Oracle:        `LIMIT n` → `FETCH FIRST n ROWS ONLY`
1229/// - All others:    `TOP n` / `FETCH FIRST n` → `LIMIT n`
1230fn transform_limit(sel: &mut SelectStatement, target: Dialect) {
1231    if is_tsql_family(target) {
1232        // Move LIMIT → TOP for T-SQL (only when there's no OFFSET)
1233        if let Some(limit) = sel.limit.take() {
1234            if sel.offset.is_none() {
1235                sel.top = Some(Box::new(limit));
1236            } else {
1237                // T-SQL with OFFSET uses OFFSET n ROWS FETCH NEXT m ROWS ONLY
1238                sel.fetch_first = Some(limit);
1239                // T-SQL OFFSET/FETCH requires ORDER BY. Add ORDER BY (SELECT NULL) if absent.
1240                if sel.order_by.is_empty() {
1241                    sel.order_by = vec![OrderByItem {
1242                        expr: Expr::Subquery(Box::new(Statement::Select(SelectStatement {
1243                            comments: Vec::new(),
1244                            ctes: Vec::new(),
1245                            distinct: false,
1246                            top: None,
1247                            columns: vec![SelectItem::Expr {
1248                                expr: Expr::Null,
1249                                alias: None,
1250                                alias_quote_style: QuoteStyle::None,
1251                            }],
1252                            from: None,
1253                            joins: Vec::new(),
1254                            where_clause: None,
1255                            group_by: Vec::new(),
1256                            having: None,
1257                            order_by: Vec::new(),
1258                            limit: None,
1259                            offset: None,
1260                            fetch_first: None,
1261                            qualify: None,
1262                            window_definitions: Vec::new(),
1263                        }))),
1264                        ascending: true,
1265                        nulls_first: None,
1266                    }];
1267                }
1268            }
1269        }
1270        // Also move fetch_first → top when no offset
1271        if sel.offset.is_none() {
1272            if let Some(fetch) = sel.fetch_first.take() {
1273                sel.top = Some(Box::new(fetch));
1274            }
1275        }
1276    } else if matches!(target, Dialect::Oracle) {
1277        // Oracle prefers FETCH FIRST n ROWS ONLY (SQL:2008 syntax)
1278        if let Some(limit) = sel.limit.take() {
1279            sel.fetch_first = Some(limit);
1280        }
1281        if let Some(top) = sel.top.take() {
1282            sel.fetch_first = Some(*top);
1283        }
1284    } else {
1285        // All other dialects: normalize to LIMIT
1286        if let Some(top) = sel.top.take() {
1287            if sel.limit.is_none() {
1288                sel.limit = Some(*top);
1289            }
1290        }
1291        if let Some(fetch) = sel.fetch_first.take() {
1292            if sel.limit.is_none() {
1293                sel.limit = Some(fetch);
1294            }
1295        }
1296    }
1297}
1298
1299// ═══════════════════════════════════════════════════════════════════════════
1300// Quoted-identifier transform
1301// ═══════════════════════════════════════════════════════════════════════════
1302
1303/// Convert any quoted identifiers in expressions to the target dialect's
1304/// quoting convention.
1305fn transform_quotes(expr: Expr, target: Dialect) -> Expr {
1306    match expr {
1307        Expr::Column {
1308            table,
1309            name,
1310            quote_style,
1311            table_quote_style,
1312        } => {
1313            let new_qs = if quote_style.is_quoted() {
1314                QuoteStyle::for_dialect(target)
1315            } else {
1316                QuoteStyle::None
1317            };
1318            let new_tqs = if table_quote_style.is_quoted() {
1319                QuoteStyle::for_dialect(target)
1320            } else {
1321                QuoteStyle::None
1322            };
1323            Expr::Column {
1324                table,
1325                name,
1326                quote_style: new_qs,
1327                table_quote_style: new_tqs,
1328            }
1329        }
1330        // Recurse into sub-expressions
1331        Expr::BinaryOp { left, op, right } => Expr::BinaryOp {
1332            left: Box::new(transform_quotes(*left, target)),
1333            op,
1334            right: Box::new(transform_quotes(*right, target)),
1335        },
1336        Expr::UnaryOp { op, expr } => Expr::UnaryOp {
1337            op,
1338            expr: Box::new(transform_quotes(*expr, target)),
1339        },
1340        Expr::Function {
1341            name,
1342            args,
1343            distinct,
1344            filter,
1345            over,
1346            order_by,
1347            within_group,
1348        } => Expr::Function {
1349            name,
1350            args: args
1351                .into_iter()
1352                .map(|a| transform_quotes(a, target))
1353                .collect(),
1354            distinct,
1355            filter: filter.map(|f| Box::new(transform_quotes(*f, target))),
1356            over,
1357            order_by,
1358            within_group,
1359        },
1360        Expr::TypedFunction { func, filter, over } => Expr::TypedFunction {
1361            func: func.transform_children(&|e| transform_quotes(e, target)),
1362            filter: filter.map(|f| Box::new(transform_quotes(*f, target))),
1363            over,
1364        },
1365        Expr::Nested(inner) => Expr::Nested(Box::new(transform_quotes(*inner, target))),
1366        Expr::Alias { expr, name } => Expr::Alias {
1367            expr: Box::new(transform_quotes(*expr, target)),
1368            name,
1369        },
1370        other => other,
1371    }
1372}
1373
1374/// Transform quoting for all identifier-bearing nodes inside a SELECT.
1375fn transform_quotes_in_select(sel: &mut SelectStatement, target: Dialect) {
1376    // Columns in the select list
1377    for item in &mut sel.columns {
1378        if let SelectItem::Expr { expr, .. } = item {
1379            *expr = transform_quotes(expr.clone(), target);
1380        }
1381    }
1382    // WHERE
1383    if let Some(wh) = &mut sel.where_clause {
1384        *wh = transform_quotes(wh.clone(), target);
1385    }
1386    // GROUP BY
1387    for gb in &mut sel.group_by {
1388        *gb = transform_quotes(gb.clone(), target);
1389    }
1390    // HAVING
1391    if let Some(having) = &mut sel.having {
1392        *having = transform_quotes(having.clone(), target);
1393    }
1394    // ORDER BY
1395    for ob in &mut sel.order_by {
1396        ob.expr = transform_quotes(ob.expr.clone(), target);
1397    }
1398    // Table refs (FROM, JOINs)
1399    if let Some(from) = &mut sel.from {
1400        transform_quotes_in_table_source(&mut from.source, target);
1401    }
1402    for join in &mut sel.joins {
1403        transform_quotes_in_table_source(&mut join.table, target);
1404        if let Some(on) = &mut join.on {
1405            *on = transform_quotes(on.clone(), target);
1406        }
1407    }
1408}
1409
1410fn transform_quotes_in_table_source(source: &mut TableSource, target: Dialect) {
1411    match source {
1412        TableSource::Table(tref) => {
1413            if tref.name_quote_style.is_quoted() {
1414                tref.name_quote_style = QuoteStyle::for_dialect(target);
1415            }
1416        }
1417        TableSource::Subquery { .. } => {}
1418        TableSource::TableFunction { .. } => {}
1419        TableSource::Lateral { source } => transform_quotes_in_table_source(source, target),
1420        TableSource::Pivot { source, .. } | TableSource::Unpivot { source, .. } => {
1421            transform_quotes_in_table_source(source, target);
1422        }
1423        TableSource::Unnest { .. } => {}
1424    }
1425}
1426
1427// ═══════════════════════════════════════════════════════════════════════════
1428// Concat operator transform (Change 3: || → CONCAT() for T-SQL)
1429// ═══════════════════════════════════════════════════════════════════════════
1430
1431/// Collect all operands from a chain of `||` (Concat) operations into a flat list.
1432fn collect_concat_args(expr: &Expr, args: &mut Vec<Expr>) {
1433    match expr {
1434        Expr::BinaryOp {
1435            left,
1436            op: BinaryOperator::Concat,
1437            right,
1438        } => {
1439            collect_concat_args(left, args);
1440            collect_concat_args(right, args);
1441        }
1442        other => args.push(other.clone()),
1443    }
1444}
1445
1446// ═══════════════════════════════════════════════════════════════════════════
1447// Interval arithmetic transform (Change 6: expr ± INTERVAL → DATEADD())
1448// ═══════════════════════════════════════════════════════════════════════════
1449
1450/// Try to transform `expr ± INTERVAL 'n unit'` into `DATEADD(unit, ±n, expr)` for T-SQL.
1451/// Returns `Some(transformed_expr)` if the right side is an interval, `None` otherwise.
1452fn try_transform_interval_arithmetic(
1453    left: &Expr,
1454    op: &BinaryOperator,
1455    right: &Expr,
1456) -> Option<Expr> {
1457    // Check right side is an interval
1458    if let Expr::Interval { value, unit } = right {
1459        if let Some((count, unit_name)) = parse_interval_value(value, unit) {
1460            let final_count = if matches!(op, BinaryOperator::Minus) {
1461                -count
1462            } else {
1463                count
1464            };
1465            return Some(Expr::Function {
1466                name: "DATEADD".to_string(),
1467                args: vec![
1468                    // Use a Column expr for the datepart keyword (unquoted identifier)
1469                    Expr::Column {
1470                        table: None,
1471                        name: unit_name,
1472                        quote_style: QuoteStyle::None,
1473                        table_quote_style: QuoteStyle::None,
1474                    },
1475                    Expr::Number(final_count.to_string()),
1476                    left.clone(),
1477                ],
1478                distinct: false,
1479                filter: None,
1480                over: None,
1481                order_by: vec![],
1482                within_group: false,
1483            });
1484        }
1485    }
1486
1487    // Check left side is an interval (less common: INTERVAL '7 days' + col)
1488    if let Expr::Interval { value, unit } = left {
1489        if matches!(op, BinaryOperator::Plus) {
1490            if let Some((count, unit_name)) = parse_interval_value(value, unit) {
1491                return Some(Expr::Function {
1492                    name: "DATEADD".to_string(),
1493                    args: vec![
1494                        Expr::Column {
1495                            table: None,
1496                            name: unit_name,
1497                            quote_style: QuoteStyle::None,
1498                            table_quote_style: QuoteStyle::None,
1499                        },
1500                        Expr::Number(count.to_string()),
1501                        right.clone(),
1502                    ],
1503                    distinct: false,
1504                    filter: None,
1505                    over: None,
1506                    order_by: vec![],
1507                    within_group: false,
1508                });
1509            }
1510        }
1511    }
1512
1513    None
1514}
1515
1516/// Parse an interval value expression and optional unit into (count, T-SQL datepart name).
1517fn parse_interval_value(value: &Expr, unit: &Option<DateTimeField>) -> Option<(i64, String)> {
1518    // Case 1: INTERVAL '7 days' (value is a string literal containing "7 days")
1519    if let Expr::StringLiteral(s) = value {
1520        let parts: Vec<&str> = s.trim().split_whitespace().collect();
1521        if parts.len() == 2 {
1522            let count: i64 = parts[0].parse().ok()?;
1523            let unit_name = normalize_interval_unit(parts[1])?;
1524            return Some((count, unit_name));
1525        }
1526        if parts.len() == 1 {
1527            // Just a number in the string, unit must come from the `unit` field
1528            let count: i64 = parts[0].parse().ok()?;
1529            if let Some(u) = unit {
1530                let unit_name = datetime_field_to_tsql(u)?;
1531                return Some((count, unit_name));
1532            }
1533        }
1534    }
1535
1536    // Case 2: INTERVAL 7 DAY (value is a number, unit is DateTimeField)
1537    if let Expr::Number(n) = value {
1538        let count: i64 = n.parse().ok()?;
1539        if let Some(u) = unit {
1540            let unit_name = datetime_field_to_tsql(u)?;
1541            return Some((count, unit_name));
1542        }
1543    }
1544
1545    None
1546}
1547
1548/// Normalize an interval unit string to a T-SQL DATEADD part name.
1549fn normalize_interval_unit(unit: &str) -> Option<String> {
1550    let lower = unit.to_lowercase();
1551    let normalized = lower.trim_end_matches('s');
1552    match normalized {
1553        "year" => Some("YEAR".to_string()),
1554        "month" => Some("MONTH".to_string()),
1555        "week" => Some("WEEK".to_string()),
1556        "day" => Some("DAY".to_string()),
1557        "hour" => Some("HOUR".to_string()),
1558        "minute" => Some("MINUTE".to_string()),
1559        "second" => Some("SECOND".to_string()),
1560        "millisecond" => Some("MILLISECOND".to_string()),
1561        "microsecond" => Some("MICROSECOND".to_string()),
1562        _ => None,
1563    }
1564}
1565
1566/// Convert a DateTimeField to T-SQL DATEADD unit name.
1567fn datetime_field_to_tsql(field: &DateTimeField) -> Option<String> {
1568    match field {
1569        DateTimeField::Year => Some("YEAR".to_string()),
1570        DateTimeField::Quarter => Some("QUARTER".to_string()),
1571        DateTimeField::Month => Some("MONTH".to_string()),
1572        DateTimeField::Week => Some("WEEK".to_string()),
1573        DateTimeField::Day => Some("DAY".to_string()),
1574        DateTimeField::Hour => Some("HOUR".to_string()),
1575        DateTimeField::Minute => Some("MINUTE".to_string()),
1576        DateTimeField::Second => Some("SECOND".to_string()),
1577        DateTimeField::Millisecond => Some("MILLISECOND".to_string()),
1578        DateTimeField::Microsecond => Some("MICROSECOND".to_string()),
1579        _ => None,
1580    }
1581}
1582
1583// ═══════════════════════════════════════════════════════════════════════════
1584// SIMILAR TO → LIKE pattern simplification (Change 9)
1585// ═══════════════════════════════════════════════════════════════════════════
1586
1587/// Simplify a SIMILAR TO pattern for use with LIKE.
1588/// Strips regex features (|, (), +, *) that T-SQL LIKE doesn't support.
1589fn simplify_similar_to_pattern(pattern: &Expr) -> Expr {
1590    if let Expr::StringLiteral(s) = pattern {
1591        let simplified = s.replace('|', "%").replace('(', "").replace(')', "");
1592        Expr::StringLiteral(simplified)
1593    } else {
1594        pattern.clone()
1595    }
1596}