Skip to main content

sqlglot_rust/dialects/
mod.rs

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