Skip to main content

sqlglot_rust/dialects/
mod.rs

1use serde::{Deserialize, Serialize};
2
3use crate::ast::*;
4
5pub mod time;
6
7/// Supported SQL dialects.
8///
9/// Mirrors the full set of dialects supported by Python's sqlglot library.
10/// Dialects are grouped into **Official** (core, higher-priority maintenance)
11/// and **Community** (contributed, fully functional) tiers.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub enum Dialect {
14    // ── Core / base ──────────────────────────────────────────────────────
15    /// ANSI SQL standard (default / base dialect)
16    Ansi,
17
18    // ── Official dialects ────────────────────────────────────────────────
19    /// AWS Athena (Presto-based)
20    Athena,
21    /// Google BigQuery
22    BigQuery,
23    /// ClickHouse
24    ClickHouse,
25    /// Databricks (Spark-based)
26    Databricks,
27    /// DuckDB
28    DuckDb,
29    /// Apache Hive
30    Hive,
31    /// MySQL
32    Mysql,
33    /// Oracle Database
34    Oracle,
35    /// PostgreSQL
36    Postgres,
37    /// Presto
38    Presto,
39    /// Amazon Redshift (Postgres-based)
40    Redshift,
41    /// Snowflake
42    Snowflake,
43    /// Apache Spark SQL
44    Spark,
45    /// SQLite
46    Sqlite,
47    /// StarRocks (MySQL-compatible)
48    StarRocks,
49    /// Trino (Presto successor)
50    Trino,
51    /// Microsoft SQL Server (T-SQL)
52    Tsql,
53
54    // ── Community dialects ───────────────────────────────────────────────
55    /// Apache Doris (MySQL-compatible)
56    Doris,
57    /// Dremio
58    Dremio,
59    /// Apache Drill
60    Drill,
61    /// Apache Druid
62    Druid,
63    /// Exasol
64    Exasol,
65    /// Microsoft Fabric (T-SQL variant)
66    Fabric,
67    /// Materialize (Postgres-compatible)
68    Materialize,
69    /// PRQL (Pipelined Relational Query Language)
70    Prql,
71    /// RisingWave (Postgres-compatible)
72    RisingWave,
73    /// SingleStore (MySQL-compatible)
74    SingleStore,
75    /// Tableau
76    Tableau,
77    /// Teradata
78    Teradata,
79}
80
81impl std::fmt::Display for Dialect {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        match self {
84            Dialect::Ansi => write!(f, "ANSI SQL"),
85            Dialect::Athena => write!(f, "Athena"),
86            Dialect::BigQuery => write!(f, "BigQuery"),
87            Dialect::ClickHouse => write!(f, "ClickHouse"),
88            Dialect::Databricks => write!(f, "Databricks"),
89            Dialect::DuckDb => write!(f, "DuckDB"),
90            Dialect::Hive => write!(f, "Hive"),
91            Dialect::Mysql => write!(f, "MySQL"),
92            Dialect::Oracle => write!(f, "Oracle"),
93            Dialect::Postgres => write!(f, "PostgreSQL"),
94            Dialect::Presto => write!(f, "Presto"),
95            Dialect::Redshift => write!(f, "Redshift"),
96            Dialect::Snowflake => write!(f, "Snowflake"),
97            Dialect::Spark => write!(f, "Spark"),
98            Dialect::Sqlite => write!(f, "SQLite"),
99            Dialect::StarRocks => write!(f, "StarRocks"),
100            Dialect::Trino => write!(f, "Trino"),
101            Dialect::Tsql => write!(f, "T-SQL"),
102            Dialect::Doris => write!(f, "Doris"),
103            Dialect::Dremio => write!(f, "Dremio"),
104            Dialect::Drill => write!(f, "Drill"),
105            Dialect::Druid => write!(f, "Druid"),
106            Dialect::Exasol => write!(f, "Exasol"),
107            Dialect::Fabric => write!(f, "Fabric"),
108            Dialect::Materialize => write!(f, "Materialize"),
109            Dialect::Prql => write!(f, "PRQL"),
110            Dialect::RisingWave => write!(f, "RisingWave"),
111            Dialect::SingleStore => write!(f, "SingleStore"),
112            Dialect::Tableau => write!(f, "Tableau"),
113            Dialect::Teradata => write!(f, "Teradata"),
114        }
115    }
116}
117
118impl Dialect {
119    /// Returns the support tier for this dialect.
120    #[must_use]
121    pub fn support_level(&self) -> &'static str {
122        match self {
123            Dialect::Ansi
124            | Dialect::Athena
125            | Dialect::BigQuery
126            | Dialect::ClickHouse
127            | Dialect::Databricks
128            | Dialect::DuckDb
129            | Dialect::Hive
130            | Dialect::Mysql
131            | Dialect::Oracle
132            | Dialect::Postgres
133            | Dialect::Presto
134            | Dialect::Redshift
135            | Dialect::Snowflake
136            | Dialect::Spark
137            | Dialect::Sqlite
138            | Dialect::StarRocks
139            | Dialect::Trino
140            | Dialect::Tsql => "Official",
141
142            Dialect::Doris
143            | Dialect::Dremio
144            | Dialect::Drill
145            | Dialect::Druid
146            | Dialect::Exasol
147            | Dialect::Fabric
148            | Dialect::Materialize
149            | Dialect::Prql
150            | Dialect::RisingWave
151            | Dialect::SingleStore
152            | Dialect::Tableau
153            | Dialect::Teradata => "Community",
154        }
155    }
156
157    /// Returns all dialect variants.
158    #[must_use]
159    pub fn all() -> &'static [Dialect] {
160        &[
161            Dialect::Ansi,
162            Dialect::Athena,
163            Dialect::BigQuery,
164            Dialect::ClickHouse,
165            Dialect::Databricks,
166            Dialect::Doris,
167            Dialect::Dremio,
168            Dialect::Drill,
169            Dialect::Druid,
170            Dialect::DuckDb,
171            Dialect::Exasol,
172            Dialect::Fabric,
173            Dialect::Hive,
174            Dialect::Materialize,
175            Dialect::Mysql,
176            Dialect::Oracle,
177            Dialect::Postgres,
178            Dialect::Presto,
179            Dialect::Prql,
180            Dialect::Redshift,
181            Dialect::RisingWave,
182            Dialect::SingleStore,
183            Dialect::Snowflake,
184            Dialect::Spark,
185            Dialect::Sqlite,
186            Dialect::StarRocks,
187            Dialect::Tableau,
188            Dialect::Teradata,
189            Dialect::Trino,
190            Dialect::Tsql,
191        ]
192    }
193
194    /// Parse a dialect name (case-insensitive) into a `Dialect`.
195    pub fn from_str(s: &str) -> Option<Dialect> {
196        match s.to_lowercase().as_str() {
197            "" | "ansi" => Some(Dialect::Ansi),
198            "athena" => Some(Dialect::Athena),
199            "bigquery" => Some(Dialect::BigQuery),
200            "clickhouse" => Some(Dialect::ClickHouse),
201            "databricks" => Some(Dialect::Databricks),
202            "doris" => Some(Dialect::Doris),
203            "dremio" => Some(Dialect::Dremio),
204            "drill" => Some(Dialect::Drill),
205            "druid" => Some(Dialect::Druid),
206            "duckdb" => Some(Dialect::DuckDb),
207            "exasol" => Some(Dialect::Exasol),
208            "fabric" => Some(Dialect::Fabric),
209            "hive" => Some(Dialect::Hive),
210            "materialize" => Some(Dialect::Materialize),
211            "mysql" => Some(Dialect::Mysql),
212            "oracle" => Some(Dialect::Oracle),
213            "postgres" | "postgresql" => Some(Dialect::Postgres),
214            "presto" => Some(Dialect::Presto),
215            "prql" => Some(Dialect::Prql),
216            "redshift" => Some(Dialect::Redshift),
217            "risingwave" => Some(Dialect::RisingWave),
218            "singlestore" => Some(Dialect::SingleStore),
219            "snowflake" => Some(Dialect::Snowflake),
220            "spark" => Some(Dialect::Spark),
221            "sqlite" => Some(Dialect::Sqlite),
222            "starrocks" => Some(Dialect::StarRocks),
223            "tableau" => Some(Dialect::Tableau),
224            "teradata" => Some(Dialect::Teradata),
225            "trino" => Some(Dialect::Trino),
226            "tsql" | "mssql" | "sqlserver" => Some(Dialect::Tsql),
227            _ => None,
228        }
229    }
230}
231
232// ═══════════════════════════════════════════════════════════════════════════
233// Dialect families — helpers for grouping similar dialects
234// ═══════════════════════════════════════════════════════════════════════════
235
236/// Dialects in the MySQL family (use SUBSTR, IFNULL, similar type system).
237fn is_mysql_family(d: Dialect) -> bool {
238    matches!(
239        d,
240        Dialect::Mysql | Dialect::Doris | Dialect::SingleStore | Dialect::StarRocks
241    )
242}
243
244/// Dialects in the Postgres family (support ILIKE, BYTEA, SUBSTRING).
245fn is_postgres_family(d: Dialect) -> bool {
246    matches!(
247        d,
248        Dialect::Postgres | Dialect::Redshift | Dialect::Materialize | Dialect::RisingWave
249    )
250}
251
252/// Dialects in the Presto family (ANSI-like, VARCHAR oriented).
253fn is_presto_family(d: Dialect) -> bool {
254    matches!(d, Dialect::Presto | Dialect::Trino | Dialect::Athena)
255}
256
257/// Dialects in the Hive/Spark family (use STRING type, SUBSTR).
258fn is_hive_family(d: Dialect) -> bool {
259    matches!(d, Dialect::Hive | Dialect::Spark | Dialect::Databricks)
260}
261
262/// Dialects in the T-SQL family.
263fn is_tsql_family(d: Dialect) -> bool {
264    matches!(d, Dialect::Tsql | Dialect::Fabric)
265}
266
267/// Dialects that natively support ILIKE.
268fn supports_ilike(d: Dialect) -> bool {
269    matches!(
270        d,
271        Dialect::Postgres
272            | Dialect::Redshift
273            | Dialect::Materialize
274            | Dialect::RisingWave
275            | Dialect::DuckDb
276            | Dialect::Snowflake
277            | Dialect::ClickHouse
278            | Dialect::Trino
279            | Dialect::Presto
280            | Dialect::Athena
281            | Dialect::Databricks
282            | Dialect::Spark
283            | Dialect::Hive
284            | Dialect::StarRocks
285            | Dialect::Exasol
286            | Dialect::Druid
287            | Dialect::Dremio
288    )
289}
290
291// ═══════════════════════════════════════════════════════════════════════════
292// Statement / expression transforms
293// ═══════════════════════════════════════════════════════════════════════════
294
295/// Transform a statement from one dialect to another.
296///
297/// This applies dialect-specific rewrite rules such as:
298/// - Type mapping (e.g., `TEXT` → `STRING` for BigQuery)
299/// - Function name mapping (e.g., `NOW()` → `CURRENT_TIMESTAMP()`)
300/// - ILIKE → LIKE with LOWER() wrapping for dialects that don't support ILIKE
301#[must_use]
302pub fn transform(statement: &Statement, from: Dialect, to: Dialect) -> Statement {
303    if from == to {
304        return statement.clone();
305    }
306    let mut stmt = statement.clone();
307    transform_statement(&mut stmt, to);
308    stmt
309}
310
311fn transform_statement(statement: &mut Statement, target: Dialect) {
312    match statement {
313        Statement::Select(sel) => {
314            // Transform LIMIT / TOP / FETCH FIRST for the target dialect
315            transform_limit(sel, target);
316            // Transform identifier quoting for the target dialect
317            transform_quotes_in_select(sel, target);
318
319            for item in &mut sel.columns {
320                if let SelectItem::Expr { expr, .. } = item {
321                    *expr = transform_expr(expr.clone(), target);
322                }
323            }
324            if let Some(wh) = &mut sel.where_clause {
325                *wh = transform_expr(wh.clone(), target);
326            }
327            for gb in &mut sel.group_by {
328                *gb = transform_expr(gb.clone(), target);
329            }
330            if let Some(having) = &mut sel.having {
331                *having = transform_expr(having.clone(), target);
332            }
333        }
334        Statement::Insert(ins) => {
335            if let InsertSource::Values(rows) = &mut ins.source {
336                for row in rows {
337                    for val in row {
338                        *val = transform_expr(val.clone(), target);
339                    }
340                }
341            }
342        }
343        Statement::Update(upd) => {
344            for (_, val) in &mut upd.assignments {
345                *val = transform_expr(val.clone(), target);
346            }
347            if let Some(wh) = &mut upd.where_clause {
348                *wh = transform_expr(wh.clone(), target);
349            }
350        }
351        // DDL: map data types in CREATE TABLE column definitions
352        Statement::CreateTable(ct) => {
353            for col in &mut ct.columns {
354                col.data_type = map_data_type(col.data_type.clone(), target);
355                if let Some(default) = &mut col.default {
356                    *default = transform_expr(default.clone(), target);
357                }
358            }
359            // Transform constraints (CHECK expressions)
360            for constraint in &mut ct.constraints {
361                if let TableConstraint::Check { expr, .. } = constraint {
362                    *expr = transform_expr(expr.clone(), target);
363                }
364            }
365            // Transform AS SELECT subquery
366            if let Some(as_select) = &mut ct.as_select {
367                transform_statement(as_select, target);
368            }
369        }
370        // DDL: map data types in ALTER TABLE ADD COLUMN
371        Statement::AlterTable(alt) => {
372            for action in &mut alt.actions {
373                match action {
374                    AlterTableAction::AddColumn(col) => {
375                        col.data_type = map_data_type(col.data_type.clone(), target);
376                        if let Some(default) = &mut col.default {
377                            *default = transform_expr(default.clone(), target);
378                        }
379                    }
380                    AlterTableAction::AlterColumnType { data_type, .. } => {
381                        *data_type = map_data_type(data_type.clone(), target);
382                    }
383                    _ => {}
384                }
385            }
386        }
387        _ => {}
388    }
389}
390
391/// Transform an expression for the target dialect.
392fn transform_expr(expr: Expr, target: Dialect) -> Expr {
393    match expr {
394        // Map function names across dialects
395        Expr::Function {
396            name,
397            args,
398            distinct,
399            filter,
400            over,
401        } => {
402            let new_name = map_function_name(&name, target);
403            let new_args: Vec<Expr> = args
404                .into_iter()
405                .map(|a| transform_expr(a, target))
406                .collect();
407            Expr::Function {
408                name: new_name,
409                args: new_args,
410                distinct,
411                filter: filter.map(|f| Box::new(transform_expr(*f, target))),
412                over,
413            }
414        }
415        // Recurse into typed function child expressions, with special handling
416        // for date/time formatting functions that need format string conversion
417        Expr::TypedFunction { func, filter, over } => {
418            let transformed_func = transform_typed_function(func, target);
419            Expr::TypedFunction {
420                func: transformed_func,
421                filter: filter.map(|f| Box::new(transform_expr(*f, target))),
422                over,
423            }
424        }
425        // ILIKE → LOWER(expr) LIKE LOWER(pattern) for non-supporting dialects
426        Expr::ILike {
427            expr,
428            pattern,
429            negated,
430            escape,
431        } if !supports_ilike(target) => Expr::Like {
432            expr: Box::new(Expr::TypedFunction {
433                func: TypedFunction::Lower {
434                    expr: Box::new(transform_expr(*expr, target)),
435                },
436                filter: None,
437                over: None,
438            }),
439            pattern: Box::new(Expr::TypedFunction {
440                func: TypedFunction::Lower {
441                    expr: Box::new(transform_expr(*pattern, target)),
442                },
443                filter: None,
444                over: None,
445            }),
446            negated,
447            escape,
448        },
449        // Map data types in CAST
450        Expr::Cast { expr, data_type } => Expr::Cast {
451            expr: Box::new(transform_expr(*expr, target)),
452            data_type: map_data_type(data_type, target),
453        },
454        // Recurse into binary ops
455        Expr::BinaryOp { left, op, right } => Expr::BinaryOp {
456            left: Box::new(transform_expr(*left, target)),
457            op,
458            right: Box::new(transform_expr(*right, target)),
459        },
460        Expr::UnaryOp { op, expr } => Expr::UnaryOp {
461            op,
462            expr: Box::new(transform_expr(*expr, target)),
463        },
464        Expr::Nested(inner) => Expr::Nested(Box::new(transform_expr(*inner, target))),
465        // Transform quoting on column references
466        Expr::Column {
467            table,
468            name,
469            quote_style,
470            table_quote_style,
471        } => {
472            let new_qs = if quote_style.is_quoted() {
473                QuoteStyle::for_dialect(target)
474            } else {
475                QuoteStyle::None
476            };
477            let new_tqs = if table_quote_style.is_quoted() {
478                QuoteStyle::for_dialect(target)
479            } else {
480                QuoteStyle::None
481            };
482            Expr::Column {
483                table,
484                name,
485                quote_style: new_qs,
486                table_quote_style: new_tqs,
487            }
488        }
489        // Everything else stays the same
490        other => other,
491    }
492}
493
494// ═══════════════════════════════════════════════════════════════════════════
495// Typed function transformation with format string conversion
496// ═══════════════════════════════════════════════════════════════════════════
497
498/// Transform a TypedFunction, including date/time format string conversion.
499///
500/// For TimeToStr and StrToTime functions, this converts the format string
501/// from the source dialect's convention to the target dialect's convention.
502fn transform_typed_function(func: TypedFunction, target: Dialect) -> TypedFunction {
503    match func {
504        TypedFunction::TimeToStr { expr, format } => {
505            let transformed_expr = Box::new(transform_expr(*expr, target));
506            let transformed_format = transform_format_expr(*format, target);
507            TypedFunction::TimeToStr {
508                expr: transformed_expr,
509                format: Box::new(transformed_format),
510            }
511        }
512        TypedFunction::StrToTime { expr, format } => {
513            let transformed_expr = Box::new(transform_expr(*expr, target));
514            let transformed_format = transform_format_expr(*format, target);
515            TypedFunction::StrToTime {
516                expr: transformed_expr,
517                format: Box::new(transformed_format),
518            }
519        }
520        // For all other typed functions, just transform child expressions
521        other => other.transform_children(&|e| transform_expr(e, target)),
522    }
523}
524
525/// Transform a format string expression for the target dialect.
526///
527/// If the expression is a string literal, convert the format specifiers.
528/// Otherwise, just recursively transform child expressions.
529fn transform_format_expr(expr: Expr, target: Dialect) -> Expr {
530    // We need to know the source dialect to convert properly.
531    // Since we don't have access to the source dialect here, we use heuristics
532    // to detect the format style based on the format string content.
533    match &expr {
534        Expr::StringLiteral(s) => {
535            let detected_source = detect_format_style(s);
536            let target_style = time::TimeFormatStyle::for_dialect(target);
537            
538            // Only convert if styles differ
539            if detected_source != target_style {
540                let converted = time::format_time(s, detected_source, target_style);
541                Expr::StringLiteral(converted)
542            } else {
543                expr
544            }
545        }
546        _ => transform_expr(expr, target),
547    }
548}
549
550/// Detect the format style from a format string based on its content.
551fn detect_format_style(format_str: &str) -> time::TimeFormatStyle {
552    // Check for style-specific patterns
553    if format_str.contains('%') {
554        // strftime-style format
555        if format_str.contains("%i") {
556            // MySQL uses %i for minutes
557            time::TimeFormatStyle::Mysql
558        } else {
559            // Generic strftime (SQLite, BigQuery, etc.)
560            time::TimeFormatStyle::Strftime
561        }
562    } else if format_str.contains("YYYY") || format_str.contains("yyyy") {
563        // Check for Java vs Postgres/Snowflake
564        if format_str.contains("HH24") || format_str.contains("MI") || format_str.contains("SS") {
565            // Postgres/Oracle style
566            time::TimeFormatStyle::Postgres
567        } else if format_str.contains("mm") && format_str.contains("ss") {
568            // Java style (lowercase seconds and minutes)
569            time::TimeFormatStyle::Java
570        } else if format_str.contains("FF") {
571            // Snowflake fractional seconds
572            time::TimeFormatStyle::Snowflake
573        } else if format_str.contains("MM") && format_str.contains("DD") {
574            // Could be Postgres or Snowflake - default to Postgres
575            time::TimeFormatStyle::Postgres
576        } else {
577            // Default to Java for ambiguous cases with lowercase patterns
578            time::TimeFormatStyle::Java
579        }
580    } else {
581        // Unknown format - default to strftime
582        time::TimeFormatStyle::Strftime
583    }
584}
585
586// ═══════════════════════════════════════════════════════════════════════════
587// Function name mapping
588// ═══════════════════════════════════════════════════════════════════════════
589
590/// Map function names between dialects.
591fn map_function_name(name: &str, target: Dialect) -> String {
592    let upper = name.to_uppercase();
593    match upper.as_str() {
594        // ── NOW / CURRENT_TIMESTAMP / GETDATE ────────────────────────────
595        "NOW" => {
596            if is_tsql_family(target) {
597                "GETDATE".to_string()
598            } else if matches!(
599                target,
600                Dialect::Ansi
601                    | Dialect::BigQuery
602                    | Dialect::Snowflake
603                    | Dialect::Oracle
604                    | Dialect::ClickHouse
605                    | Dialect::Exasol
606                    | Dialect::Teradata
607                    | Dialect::Druid
608                    | Dialect::Dremio
609                    | Dialect::Tableau
610            ) || is_presto_family(target)
611                || is_hive_family(target)
612            {
613                "CURRENT_TIMESTAMP".to_string()
614            } else {
615                // Postgres, MySQL, SQLite, DuckDB, Redshift, etc. – keep NOW
616                name.to_string()
617            }
618        }
619        "GETDATE" => {
620            if is_tsql_family(target) {
621                name.to_string()
622            } else if is_postgres_family(target)
623                || matches!(target, Dialect::Mysql | Dialect::DuckDb | Dialect::Sqlite)
624            {
625                "NOW".to_string()
626            } else {
627                "CURRENT_TIMESTAMP".to_string()
628            }
629        }
630
631        // ── LEN / LENGTH ─────────────────────────────────────────────────
632        "LEN" => {
633            if is_tsql_family(target) || matches!(target, Dialect::BigQuery | Dialect::Snowflake) {
634                name.to_string()
635            } else {
636                "LENGTH".to_string()
637            }
638        }
639        "LENGTH" if is_tsql_family(target) => "LEN".to_string(),
640
641        // ── SUBSTR / SUBSTRING ───────────────────────────────────────────
642        "SUBSTR" => {
643            if is_mysql_family(target)
644                || matches!(target, Dialect::Sqlite | Dialect::Oracle)
645                || is_hive_family(target)
646            {
647                "SUBSTR".to_string()
648            } else {
649                "SUBSTRING".to_string()
650            }
651        }
652        "SUBSTRING" => {
653            if is_mysql_family(target)
654                || matches!(target, Dialect::Sqlite | Dialect::Oracle)
655                || is_hive_family(target)
656            {
657                "SUBSTR".to_string()
658            } else {
659                name.to_string()
660            }
661        }
662
663        // ── IFNULL / COALESCE / ISNULL ───────────────────────────────────
664        "IFNULL" => {
665            if is_tsql_family(target) {
666                "ISNULL".to_string()
667            } else if is_mysql_family(target) || matches!(target, Dialect::Sqlite) {
668                // MySQL family + SQLite natively support IFNULL
669                name.to_string()
670            } else {
671                "COALESCE".to_string()
672            }
673        }
674        "ISNULL" => {
675            if is_tsql_family(target) {
676                name.to_string()
677            } else if is_mysql_family(target) || matches!(target, Dialect::Sqlite) {
678                "IFNULL".to_string()
679            } else {
680                "COALESCE".to_string()
681            }
682        }
683
684        // ── NVL → COALESCE (Oracle to others) ───────────────────────────
685        "NVL" => {
686            if matches!(target, Dialect::Oracle | Dialect::Snowflake) {
687                name.to_string()
688            } else if is_mysql_family(target) || matches!(target, Dialect::Sqlite) {
689                "IFNULL".to_string()
690            } else if is_tsql_family(target) {
691                "ISNULL".to_string()
692            } else {
693                "COALESCE".to_string()
694            }
695        }
696
697        // ── RANDOM / RAND ────────────────────────────────────────────────
698        "RANDOM" => {
699            if matches!(
700                target,
701                Dialect::Postgres | Dialect::Sqlite | Dialect::DuckDb
702            ) {
703                name.to_string()
704            } else {
705                "RAND".to_string()
706            }
707        }
708        "RAND" => {
709            if matches!(
710                target,
711                Dialect::Postgres | Dialect::Sqlite | Dialect::DuckDb
712            ) {
713                "RANDOM".to_string()
714            } else {
715                name.to_string()
716            }
717        }
718
719        // Everything else – preserve original name
720        _ => name.to_string(),
721    }
722}
723
724// ═══════════════════════════════════════════════════════════════════════════
725// Data-type mapping
726// ═══════════════════════════════════════════════════════════════════════════
727
728/// Map data types between dialects.
729fn map_data_type(dt: DataType, target: Dialect) -> DataType {
730    match (dt, target) {
731        // ── TEXT / STRING ────────────────────────────────────────────────
732        // TEXT → STRING for BigQuery, Hive, Spark, Databricks
733        (DataType::Text, t) if matches!(t, Dialect::BigQuery) || is_hive_family(t) => {
734            DataType::String
735        }
736        // STRING → TEXT for Postgres family, MySQL family, SQLite
737        (DataType::String, t)
738            if is_postgres_family(t) || is_mysql_family(t) || matches!(t, Dialect::Sqlite) =>
739        {
740            DataType::Text
741        }
742
743        // ── INT → BIGINT (BigQuery) ─────────────────────────────────────
744        (DataType::Int, Dialect::BigQuery) => DataType::BigInt,
745
746        // ── FLOAT → DOUBLE (BigQuery) ───────────────────────────────────
747        (DataType::Float, Dialect::BigQuery) => DataType::Double,
748
749        // ── BYTEA ↔ BLOB ────────────────────────────────────────────────
750        (DataType::Bytea, t)
751            if is_mysql_family(t)
752                || matches!(t, Dialect::Sqlite | Dialect::Oracle)
753                || is_hive_family(t) =>
754        {
755            DataType::Blob
756        }
757        (DataType::Blob, t) if is_postgres_family(t) => DataType::Bytea,
758
759        // ── BOOLEAN → BOOL ──────────────────────────────────────────────
760        (DataType::Boolean, Dialect::Mysql) => DataType::Boolean,
761
762        // Everything else is unchanged
763        (dt, _) => dt,
764    }
765}
766
767// ═══════════════════════════════════════════════════════════════════════════
768// LIMIT / TOP / FETCH FIRST transform
769// ═══════════════════════════════════════════════════════════════════════════
770
771/// Transform LIMIT / TOP / FETCH FIRST between dialects.
772///
773/// - T-SQL family:  `LIMIT n` → `TOP n` (OFFSET + FETCH handled separately)
774/// - Oracle:        `LIMIT n` → `FETCH FIRST n ROWS ONLY`
775/// - All others:    `TOP n` / `FETCH FIRST n` → `LIMIT n`
776fn transform_limit(sel: &mut SelectStatement, target: Dialect) {
777    if is_tsql_family(target) {
778        // Move LIMIT → TOP for T-SQL (only when there's no OFFSET)
779        if let Some(limit) = sel.limit.take() {
780            if sel.offset.is_none() {
781                sel.top = Some(Box::new(limit));
782            } else {
783                // T-SQL with OFFSET uses OFFSET n ROWS FETCH NEXT m ROWS ONLY
784                sel.fetch_first = Some(limit);
785            }
786        }
787        // Also move fetch_first → top when no offset
788        if sel.offset.is_none() {
789            if let Some(fetch) = sel.fetch_first.take() {
790                sel.top = Some(Box::new(fetch));
791            }
792        }
793    } else if matches!(target, Dialect::Oracle) {
794        // Oracle prefers FETCH FIRST n ROWS ONLY (SQL:2008 syntax)
795        if let Some(limit) = sel.limit.take() {
796            sel.fetch_first = Some(limit);
797        }
798        if let Some(top) = sel.top.take() {
799            sel.fetch_first = Some(*top);
800        }
801    } else {
802        // All other dialects: normalize to LIMIT
803        if let Some(top) = sel.top.take() {
804            if sel.limit.is_none() {
805                sel.limit = Some(*top);
806            }
807        }
808        if let Some(fetch) = sel.fetch_first.take() {
809            if sel.limit.is_none() {
810                sel.limit = Some(fetch);
811            }
812        }
813    }
814}
815
816// ═══════════════════════════════════════════════════════════════════════════
817// Quoted-identifier transform
818// ═══════════════════════════════════════════════════════════════════════════
819
820/// Convert any quoted identifiers in expressions to the target dialect's
821/// quoting convention.
822fn transform_quotes(expr: Expr, target: Dialect) -> Expr {
823    match expr {
824        Expr::Column {
825            table,
826            name,
827            quote_style,
828            table_quote_style,
829        } => {
830            let new_qs = if quote_style.is_quoted() {
831                QuoteStyle::for_dialect(target)
832            } else {
833                QuoteStyle::None
834            };
835            let new_tqs = if table_quote_style.is_quoted() {
836                QuoteStyle::for_dialect(target)
837            } else {
838                QuoteStyle::None
839            };
840            Expr::Column {
841                table,
842                name,
843                quote_style: new_qs,
844                table_quote_style: new_tqs,
845            }
846        }
847        // Recurse into sub-expressions
848        Expr::BinaryOp { left, op, right } => Expr::BinaryOp {
849            left: Box::new(transform_quotes(*left, target)),
850            op,
851            right: Box::new(transform_quotes(*right, target)),
852        },
853        Expr::UnaryOp { op, expr } => Expr::UnaryOp {
854            op,
855            expr: Box::new(transform_quotes(*expr, target)),
856        },
857        Expr::Function {
858            name,
859            args,
860            distinct,
861            filter,
862            over,
863        } => Expr::Function {
864            name,
865            args: args
866                .into_iter()
867                .map(|a| transform_quotes(a, target))
868                .collect(),
869            distinct,
870            filter: filter.map(|f| Box::new(transform_quotes(*f, target))),
871            over,
872        },
873        Expr::TypedFunction { func, filter, over } => Expr::TypedFunction {
874            func: func.transform_children(&|e| transform_quotes(e, target)),
875            filter: filter.map(|f| Box::new(transform_quotes(*f, target))),
876            over,
877        },
878        Expr::Nested(inner) => Expr::Nested(Box::new(transform_quotes(*inner, target))),
879        Expr::Alias { expr, name } => Expr::Alias {
880            expr: Box::new(transform_quotes(*expr, target)),
881            name,
882        },
883        other => other,
884    }
885}
886
887/// Transform quoting for all identifier-bearing nodes inside a SELECT.
888fn transform_quotes_in_select(sel: &mut SelectStatement, target: Dialect) {
889    // Columns in the select list
890    for item in &mut sel.columns {
891        if let SelectItem::Expr { expr, .. } = item {
892            *expr = transform_quotes(expr.clone(), target);
893        }
894    }
895    // WHERE
896    if let Some(wh) = &mut sel.where_clause {
897        *wh = transform_quotes(wh.clone(), target);
898    }
899    // GROUP BY
900    for gb in &mut sel.group_by {
901        *gb = transform_quotes(gb.clone(), target);
902    }
903    // HAVING
904    if let Some(having) = &mut sel.having {
905        *having = transform_quotes(having.clone(), target);
906    }
907    // ORDER BY
908    for ob in &mut sel.order_by {
909        ob.expr = transform_quotes(ob.expr.clone(), target);
910    }
911    // Table refs (FROM, JOINs)
912    if let Some(from) = &mut sel.from {
913        transform_quotes_in_table_source(&mut from.source, target);
914    }
915    for join in &mut sel.joins {
916        transform_quotes_in_table_source(&mut join.table, target);
917        if let Some(on) = &mut join.on {
918            *on = transform_quotes(on.clone(), target);
919        }
920    }
921}
922
923fn transform_quotes_in_table_source(source: &mut TableSource, target: Dialect) {
924    match source {
925        TableSource::Table(tref) => {
926            if tref.name_quote_style.is_quoted() {
927                tref.name_quote_style = QuoteStyle::for_dialect(target);
928            }
929        }
930        TableSource::Subquery { .. } => {}
931        TableSource::TableFunction { .. } => {}
932        TableSource::Lateral { source } => transform_quotes_in_table_source(source, target),
933        TableSource::Pivot { source, .. } | TableSource::Unpivot { source, .. } => {
934            transform_quotes_in_table_source(source, target);
935        }
936        TableSource::Unnest { .. } => {}
937    }
938}