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