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.
264fn is_tsql_family(d: Dialect) -> bool {
265    matches!(d, Dialect::Tsql | Dialect::Fabric)
266}
267
268/// Dialects that natively support ILIKE.
269pub(crate) fn supports_ilike_builtin(d: Dialect) -> bool {
270    matches!(
271        d,
272        Dialect::Postgres
273            | Dialect::Redshift
274            | Dialect::Materialize
275            | Dialect::RisingWave
276            | Dialect::DuckDb
277            | Dialect::Snowflake
278            | Dialect::ClickHouse
279            | Dialect::Trino
280            | Dialect::Presto
281            | Dialect::Athena
282            | Dialect::Databricks
283            | Dialect::Spark
284            | Dialect::Hive
285            | Dialect::StarRocks
286            | Dialect::Exasol
287            | Dialect::Druid
288            | Dialect::Dremio
289    )
290}
291
292// ═══════════════════════════════════════════════════════════════════════════
293// Statement / expression transforms
294// ═══════════════════════════════════════════════════════════════════════════
295
296/// Transform a statement from one dialect to another.
297///
298/// This applies dialect-specific rewrite rules such as:
299/// - Type mapping (e.g., `TEXT` → `STRING` for BigQuery)
300/// - Function name mapping (e.g., `NOW()` → `CURRENT_TIMESTAMP()`)
301/// - ILIKE → LIKE with LOWER() wrapping for dialects that don't support ILIKE
302#[must_use]
303pub fn transform(statement: &Statement, from: Dialect, to: Dialect) -> Statement {
304    if from == to {
305        return statement.clone();
306    }
307    let mut stmt = statement.clone();
308    transform_statement(&mut stmt, to);
309    stmt
310}
311
312fn transform_statement(statement: &mut Statement, target: Dialect) {
313    match statement {
314        Statement::Select(sel) => {
315            // Transform LIMIT / TOP / FETCH FIRST for the target dialect
316            transform_limit(sel, target);
317            // Transform identifier quoting for the target dialect
318            transform_quotes_in_select(sel, target);
319
320            for item in &mut sel.columns {
321                if let SelectItem::Expr { expr, .. } = item {
322                    *expr = transform_expr(expr.clone(), target);
323                }
324            }
325            if let Some(wh) = &mut sel.where_clause {
326                *wh = transform_expr(wh.clone(), target);
327            }
328            for gb in &mut sel.group_by {
329                *gb = transform_expr(gb.clone(), target);
330            }
331            if let Some(having) = &mut sel.having {
332                *having = transform_expr(having.clone(), target);
333            }
334        }
335        Statement::Insert(ins) => {
336            if let InsertSource::Values(rows) = &mut ins.source {
337                for row in rows {
338                    for val in row {
339                        *val = transform_expr(val.clone(), target);
340                    }
341                }
342            }
343        }
344        Statement::Update(upd) => {
345            for (_, val) in &mut upd.assignments {
346                *val = transform_expr(val.clone(), target);
347            }
348            if let Some(wh) = &mut upd.where_clause {
349                *wh = transform_expr(wh.clone(), target);
350            }
351        }
352        // DDL: map data types in CREATE TABLE column definitions
353        Statement::CreateTable(ct) => {
354            for col in &mut ct.columns {
355                col.data_type = map_data_type(col.data_type.clone(), target);
356                if let Some(default) = &mut col.default {
357                    *default = transform_expr(default.clone(), target);
358                }
359            }
360            // Transform constraints (CHECK expressions)
361            for constraint in &mut ct.constraints {
362                if let TableConstraint::Check { expr, .. } = constraint {
363                    *expr = transform_expr(expr.clone(), target);
364                }
365            }
366            // Transform AS SELECT subquery
367            if let Some(as_select) = &mut ct.as_select {
368                transform_statement(as_select, target);
369            }
370        }
371        // DDL: map data types in ALTER TABLE ADD COLUMN
372        Statement::AlterTable(alt) => {
373            for action in &mut alt.actions {
374                match action {
375                    AlterTableAction::AddColumn(col) => {
376                        col.data_type = map_data_type(col.data_type.clone(), target);
377                        if let Some(default) = &mut col.default {
378                            *default = transform_expr(default.clone(), target);
379                        }
380                    }
381                    AlterTableAction::AlterColumnType { data_type, .. } => {
382                        *data_type = map_data_type(data_type.clone(), target);
383                    }
384                    _ => {}
385                }
386            }
387        }
388        _ => {}
389    }
390}
391
392/// Transform an expression for the target dialect.
393fn transform_expr(expr: Expr, target: Dialect) -> Expr {
394    match expr {
395        // Map function names across dialects
396        Expr::Function {
397            name,
398            args,
399            distinct,
400            filter,
401            over,
402        } => {
403            let new_name = map_function_name(&name, target);
404            let new_args: Vec<Expr> = args
405                .into_iter()
406                .map(|a| transform_expr(a, target))
407                .collect();
408            Expr::Function {
409                name: new_name,
410                args: new_args,
411                distinct,
412                filter: filter.map(|f| Box::new(transform_expr(*f, target))),
413                over,
414            }
415        }
416        // Recurse into typed function child expressions, with special handling
417        // for date/time formatting functions that need format string conversion
418        Expr::TypedFunction { func, filter, over } => {
419            let transformed_func = transform_typed_function(func, target);
420            Expr::TypedFunction {
421                func: transformed_func,
422                filter: filter.map(|f| Box::new(transform_expr(*f, target))),
423                over,
424            }
425        }
426        // ILIKE → LOWER(expr) LIKE LOWER(pattern) for non-supporting dialects
427        Expr::ILike {
428            expr,
429            pattern,
430            negated,
431            escape,
432        } if !supports_ilike_builtin(target) => Expr::Like {
433            expr: Box::new(Expr::TypedFunction {
434                func: TypedFunction::Lower {
435                    expr: Box::new(transform_expr(*expr, target)),
436                },
437                filter: None,
438                over: None,
439            }),
440            pattern: Box::new(Expr::TypedFunction {
441                func: TypedFunction::Lower {
442                    expr: Box::new(transform_expr(*pattern, target)),
443                },
444                filter: None,
445                over: None,
446            }),
447            negated,
448            escape,
449        },
450        // Map data types in CAST
451        Expr::Cast { expr, data_type } => Expr::Cast {
452            expr: Box::new(transform_expr(*expr, target)),
453            data_type: map_data_type(data_type, target),
454        },
455        // Recurse into binary ops
456        Expr::BinaryOp { left, op, right } => Expr::BinaryOp {
457            left: Box::new(transform_expr(*left, target)),
458            op,
459            right: Box::new(transform_expr(*right, target)),
460        },
461        Expr::UnaryOp { op, expr } => Expr::UnaryOp {
462            op,
463            expr: Box::new(transform_expr(*expr, target)),
464        },
465        Expr::Nested(inner) => Expr::Nested(Box::new(transform_expr(*inner, target))),
466        // Transform quoting on column references
467        Expr::Column {
468            table,
469            name,
470            quote_style,
471            table_quote_style,
472        } => {
473            let new_qs = if quote_style.is_quoted() {
474                QuoteStyle::for_dialect(target)
475            } else {
476                QuoteStyle::None
477            };
478            let new_tqs = if table_quote_style.is_quoted() {
479                QuoteStyle::for_dialect(target)
480            } else {
481                QuoteStyle::None
482            };
483            Expr::Column {
484                table,
485                name,
486                quote_style: new_qs,
487                table_quote_style: new_tqs,
488            }
489        }
490        // Everything else stays the same
491        other => other,
492    }
493}
494
495// ═══════════════════════════════════════════════════════════════════════════
496// Typed function transformation with format string conversion
497// ═══════════════════════════════════════════════════════════════════════════
498
499/// Transform a TypedFunction, including date/time format string conversion.
500///
501/// For TimeToStr and StrToTime functions, this converts the format string
502/// from the source dialect's convention to the target dialect's convention.
503fn transform_typed_function(func: TypedFunction, target: Dialect) -> TypedFunction {
504    match func {
505        TypedFunction::TimeToStr { expr, format } => {
506            let transformed_expr = Box::new(transform_expr(*expr, target));
507            let transformed_format = transform_format_expr(*format, target);
508            TypedFunction::TimeToStr {
509                expr: transformed_expr,
510                format: Box::new(transformed_format),
511            }
512        }
513        TypedFunction::StrToTime { expr, format } => {
514            let transformed_expr = Box::new(transform_expr(*expr, target));
515            let transformed_format = transform_format_expr(*format, target);
516            TypedFunction::StrToTime {
517                expr: transformed_expr,
518                format: Box::new(transformed_format),
519            }
520        }
521        // For all other typed functions, just transform child expressions
522        other => other.transform_children(&|e| transform_expr(e, target)),
523    }
524}
525
526/// Transform a format string expression for the target dialect.
527///
528/// If the expression is a string literal, convert the format specifiers.
529/// Otherwise, just recursively transform child expressions.
530fn transform_format_expr(expr: Expr, target: Dialect) -> Expr {
531    // We need to know the source dialect to convert properly.
532    // Since we don't have access to the source dialect here, we use heuristics
533    // to detect the format style based on the format string content.
534    match &expr {
535        Expr::StringLiteral(s) | Expr::NationalStringLiteral(s) => {
536            let detected_source = detect_format_style(s);
537            let target_style = time::TimeFormatStyle::for_dialect(target);
538
539            // Only convert if styles differ
540            if detected_source != target_style {
541                let converted = time::format_time(s, detected_source, target_style);
542                match expr {
543                    Expr::NationalStringLiteral(_) => Expr::NationalStringLiteral(converted),
544                    _ => Expr::StringLiteral(converted),
545                }
546            } else {
547                expr
548            }
549        }
550        _ => transform_expr(expr, target),
551    }
552}
553
554/// Detect the format style from a format string based on its content.
555fn detect_format_style(format_str: &str) -> time::TimeFormatStyle {
556    // Check for style-specific patterns
557    if format_str.contains('%') {
558        // strftime-style format
559        if format_str.contains("%i") {
560            // MySQL uses %i for minutes
561            time::TimeFormatStyle::Mysql
562        } else {
563            // Generic strftime (SQLite, BigQuery, etc.)
564            time::TimeFormatStyle::Strftime
565        }
566    } else if format_str.contains("YYYY") || format_str.contains("yyyy") {
567        // Check for Java vs Postgres/Snowflake
568        if format_str.contains("HH24") || format_str.contains("MI") || format_str.contains("SS") {
569            // Postgres/Oracle style
570            time::TimeFormatStyle::Postgres
571        } else if format_str.contains("mm") && format_str.contains("ss") {
572            // Java style (lowercase seconds and minutes)
573            time::TimeFormatStyle::Java
574        } else if format_str.contains("FF") {
575            // Snowflake fractional seconds
576            time::TimeFormatStyle::Snowflake
577        } else if format_str.contains("MM") && format_str.contains("DD") {
578            // Could be Postgres or Snowflake - default to Postgres
579            time::TimeFormatStyle::Postgres
580        } else {
581            // Default to Java for ambiguous cases with lowercase patterns
582            time::TimeFormatStyle::Java
583        }
584    } else {
585        // Unknown format - default to strftime
586        time::TimeFormatStyle::Strftime
587    }
588}
589
590// ═══════════════════════════════════════════════════════════════════════════
591// Function name mapping
592// ═══════════════════════════════════════════════════════════════════════════
593
594/// Map function names between dialects.
595pub(crate) fn map_function_name(name: &str, target: Dialect) -> String {
596    let upper = name.to_uppercase();
597    match upper.as_str() {
598        // ── NOW / CURRENT_TIMESTAMP / GETDATE ────────────────────────────
599        "NOW" => {
600            if is_tsql_family(target) {
601                "GETDATE".to_string()
602            } else if matches!(
603                target,
604                Dialect::Ansi
605                    | Dialect::BigQuery
606                    | Dialect::Snowflake
607                    | Dialect::Oracle
608                    | Dialect::ClickHouse
609                    | Dialect::Exasol
610                    | Dialect::Teradata
611                    | Dialect::Druid
612                    | Dialect::Dremio
613                    | Dialect::Tableau
614            ) || is_presto_family(target)
615                || is_hive_family(target)
616            {
617                "CURRENT_TIMESTAMP".to_string()
618            } else {
619                // Postgres, MySQL, SQLite, DuckDB, Redshift, etc. – keep NOW
620                name.to_string()
621            }
622        }
623        "GETDATE" => {
624            if is_tsql_family(target) {
625                name.to_string()
626            } else if is_postgres_family(target)
627                || matches!(target, Dialect::Mysql | Dialect::DuckDb | Dialect::Sqlite)
628            {
629                "NOW".to_string()
630            } else {
631                "CURRENT_TIMESTAMP".to_string()
632            }
633        }
634
635        // ── LEN / LENGTH ─────────────────────────────────────────────────
636        "LEN" => {
637            if is_tsql_family(target) || matches!(target, Dialect::BigQuery | Dialect::Snowflake) {
638                name.to_string()
639            } else {
640                "LENGTH".to_string()
641            }
642        }
643        "LENGTH" if is_tsql_family(target) => "LEN".to_string(),
644
645        // ── SUBSTR / SUBSTRING ───────────────────────────────────────────
646        "SUBSTR" => {
647            if is_mysql_family(target)
648                || matches!(target, Dialect::Sqlite | Dialect::Oracle)
649                || is_hive_family(target)
650            {
651                "SUBSTR".to_string()
652            } else {
653                "SUBSTRING".to_string()
654            }
655        }
656        "SUBSTRING" => {
657            if is_mysql_family(target)
658                || matches!(target, Dialect::Sqlite | Dialect::Oracle)
659                || is_hive_family(target)
660            {
661                "SUBSTR".to_string()
662            } else {
663                name.to_string()
664            }
665        }
666
667        // ── IFNULL / COALESCE / ISNULL ───────────────────────────────────
668        "IFNULL" => {
669            if is_tsql_family(target) {
670                "ISNULL".to_string()
671            } else if is_mysql_family(target) || matches!(target, Dialect::Sqlite) {
672                // MySQL family + SQLite natively support IFNULL
673                name.to_string()
674            } else {
675                "COALESCE".to_string()
676            }
677        }
678        "ISNULL" => {
679            if is_tsql_family(target) {
680                name.to_string()
681            } else if is_mysql_family(target) || matches!(target, Dialect::Sqlite) {
682                "IFNULL".to_string()
683            } else {
684                "COALESCE".to_string()
685            }
686        }
687
688        // ── NVL → COALESCE (Oracle to others) ───────────────────────────
689        "NVL" => {
690            if matches!(target, Dialect::Oracle | Dialect::Snowflake) {
691                name.to_string()
692            } else if is_mysql_family(target) || matches!(target, Dialect::Sqlite) {
693                "IFNULL".to_string()
694            } else if is_tsql_family(target) {
695                "ISNULL".to_string()
696            } else {
697                "COALESCE".to_string()
698            }
699        }
700
701        // ── RANDOM / RAND ────────────────────────────────────────────────
702        "RANDOM" => {
703            if matches!(
704                target,
705                Dialect::Postgres | Dialect::Sqlite | Dialect::DuckDb
706            ) {
707                name.to_string()
708            } else {
709                "RAND".to_string()
710            }
711        }
712        "RAND" => {
713            if matches!(
714                target,
715                Dialect::Postgres | Dialect::Sqlite | Dialect::DuckDb
716            ) {
717                "RANDOM".to_string()
718            } else {
719                name.to_string()
720            }
721        }
722
723        // Everything else – preserve original name
724        _ => name.to_string(),
725    }
726}
727
728// ═══════════════════════════════════════════════════════════════════════════
729// Data-type mapping
730// ═══════════════════════════════════════════════════════════════════════════
731
732/// Map data types between dialects.
733pub(crate) fn map_data_type(dt: DataType, target: Dialect) -> DataType {
734    match (dt, target) {
735        // ── TEXT / STRING ────────────────────────────────────────────────
736        // TEXT → STRING for BigQuery, Hive, Spark, Databricks
737        (DataType::Text, t) if matches!(t, Dialect::BigQuery) || is_hive_family(t) => {
738            DataType::String
739        }
740        // STRING → TEXT for Postgres family, MySQL family, SQLite
741        (DataType::String, t)
742            if is_postgres_family(t) || is_mysql_family(t) || matches!(t, Dialect::Sqlite) =>
743        {
744            DataType::Text
745        }
746
747        // ── INT → BIGINT (BigQuery) ─────────────────────────────────────
748        (DataType::Int, Dialect::BigQuery) => DataType::BigInt,
749
750        // ── FLOAT → DOUBLE (BigQuery) ───────────────────────────────────
751        (DataType::Float, Dialect::BigQuery) => DataType::Double,
752
753        // ── BYTEA ↔ BLOB ────────────────────────────────────────────────
754        (DataType::Bytea, t)
755            if is_mysql_family(t)
756                || matches!(t, Dialect::Sqlite | Dialect::Oracle)
757                || is_hive_family(t) =>
758        {
759            DataType::Blob
760        }
761        (DataType::Blob, t) if is_postgres_family(t) => DataType::Bytea,
762
763        // ── BOOLEAN → BOOL ──────────────────────────────────────────────
764        (DataType::Boolean, Dialect::Mysql) => DataType::Boolean,
765
766        // Everything else is unchanged
767        (dt, _) => dt,
768    }
769}
770
771// ═══════════════════════════════════════════════════════════════════════════
772// LIMIT / TOP / FETCH FIRST transform
773// ═══════════════════════════════════════════════════════════════════════════
774
775/// Transform LIMIT / TOP / FETCH FIRST between dialects.
776///
777/// - T-SQL family:  `LIMIT n` → `TOP n` (OFFSET + FETCH handled separately)
778/// - Oracle:        `LIMIT n` → `FETCH FIRST n ROWS ONLY`
779/// - All others:    `TOP n` / `FETCH FIRST n` → `LIMIT n`
780fn transform_limit(sel: &mut SelectStatement, target: Dialect) {
781    if is_tsql_family(target) {
782        // Move LIMIT → TOP for T-SQL (only when there's no OFFSET)
783        if let Some(limit) = sel.limit.take() {
784            if sel.offset.is_none() {
785                sel.top = Some(Box::new(limit));
786            } else {
787                // T-SQL with OFFSET uses OFFSET n ROWS FETCH NEXT m ROWS ONLY
788                sel.fetch_first = Some(limit);
789            }
790        }
791        // Also move fetch_first → top when no offset
792        if sel.offset.is_none() {
793            if let Some(fetch) = sel.fetch_first.take() {
794                sel.top = Some(Box::new(fetch));
795            }
796        }
797    } else if matches!(target, Dialect::Oracle) {
798        // Oracle prefers FETCH FIRST n ROWS ONLY (SQL:2008 syntax)
799        if let Some(limit) = sel.limit.take() {
800            sel.fetch_first = Some(limit);
801        }
802        if let Some(top) = sel.top.take() {
803            sel.fetch_first = Some(*top);
804        }
805    } else {
806        // All other dialects: normalize to LIMIT
807        if let Some(top) = sel.top.take() {
808            if sel.limit.is_none() {
809                sel.limit = Some(*top);
810            }
811        }
812        if let Some(fetch) = sel.fetch_first.take() {
813            if sel.limit.is_none() {
814                sel.limit = Some(fetch);
815            }
816        }
817    }
818}
819
820// ═══════════════════════════════════════════════════════════════════════════
821// Quoted-identifier transform
822// ═══════════════════════════════════════════════════════════════════════════
823
824/// Convert any quoted identifiers in expressions to the target dialect's
825/// quoting convention.
826fn transform_quotes(expr: Expr, target: Dialect) -> Expr {
827    match expr {
828        Expr::Column {
829            table,
830            name,
831            quote_style,
832            table_quote_style,
833        } => {
834            let new_qs = if quote_style.is_quoted() {
835                QuoteStyle::for_dialect(target)
836            } else {
837                QuoteStyle::None
838            };
839            let new_tqs = if table_quote_style.is_quoted() {
840                QuoteStyle::for_dialect(target)
841            } else {
842                QuoteStyle::None
843            };
844            Expr::Column {
845                table,
846                name,
847                quote_style: new_qs,
848                table_quote_style: new_tqs,
849            }
850        }
851        // Recurse into sub-expressions
852        Expr::BinaryOp { left, op, right } => Expr::BinaryOp {
853            left: Box::new(transform_quotes(*left, target)),
854            op,
855            right: Box::new(transform_quotes(*right, target)),
856        },
857        Expr::UnaryOp { op, expr } => Expr::UnaryOp {
858            op,
859            expr: Box::new(transform_quotes(*expr, target)),
860        },
861        Expr::Function {
862            name,
863            args,
864            distinct,
865            filter,
866            over,
867        } => Expr::Function {
868            name,
869            args: args
870                .into_iter()
871                .map(|a| transform_quotes(a, target))
872                .collect(),
873            distinct,
874            filter: filter.map(|f| Box::new(transform_quotes(*f, target))),
875            over,
876        },
877        Expr::TypedFunction { func, filter, over } => Expr::TypedFunction {
878            func: func.transform_children(&|e| transform_quotes(e, target)),
879            filter: filter.map(|f| Box::new(transform_quotes(*f, target))),
880            over,
881        },
882        Expr::Nested(inner) => Expr::Nested(Box::new(transform_quotes(*inner, target))),
883        Expr::Alias { expr, name } => Expr::Alias {
884            expr: Box::new(transform_quotes(*expr, target)),
885            name,
886        },
887        other => other,
888    }
889}
890
891/// Transform quoting for all identifier-bearing nodes inside a SELECT.
892fn transform_quotes_in_select(sel: &mut SelectStatement, target: Dialect) {
893    // Columns in the select list
894    for item in &mut sel.columns {
895        if let SelectItem::Expr { expr, .. } = item {
896            *expr = transform_quotes(expr.clone(), target);
897        }
898    }
899    // WHERE
900    if let Some(wh) = &mut sel.where_clause {
901        *wh = transform_quotes(wh.clone(), target);
902    }
903    // GROUP BY
904    for gb in &mut sel.group_by {
905        *gb = transform_quotes(gb.clone(), target);
906    }
907    // HAVING
908    if let Some(having) = &mut sel.having {
909        *having = transform_quotes(having.clone(), target);
910    }
911    // ORDER BY
912    for ob in &mut sel.order_by {
913        ob.expr = transform_quotes(ob.expr.clone(), target);
914    }
915    // Table refs (FROM, JOINs)
916    if let Some(from) = &mut sel.from {
917        transform_quotes_in_table_source(&mut from.source, target);
918    }
919    for join in &mut sel.joins {
920        transform_quotes_in_table_source(&mut join.table, target);
921        if let Some(on) = &mut join.on {
922            *on = transform_quotes(on.clone(), target);
923        }
924    }
925}
926
927fn transform_quotes_in_table_source(source: &mut TableSource, target: Dialect) {
928    match source {
929        TableSource::Table(tref) => {
930            if tref.name_quote_style.is_quoted() {
931                tref.name_quote_style = QuoteStyle::for_dialect(target);
932            }
933        }
934        TableSource::Subquery { .. } => {}
935        TableSource::TableFunction { .. } => {}
936        TableSource::Lateral { source } => transform_quotes_in_table_source(source, target),
937        TableSource::Pivot { source, .. } | TableSource::Unpivot { source, .. } => {
938            transform_quotes_in_table_source(source, target);
939        }
940        TableSource::Unnest { .. } => {}
941    }
942}