Skip to main content

polyglot_sql/dialects/
duckdb.rs

1//! DuckDB Dialect
2//!
3//! DuckDB-specific transformations based on sqlglot patterns.
4//! Key features:
5//! - Modern SQL analytics database with PostgreSQL-like syntax
6//! - LIST type for arrays
7//! - STRUCT support with dot access
8//! - EPOCH_MS / EPOCH for timestamps
9//! - EXCLUDE / REPLACE in SELECT
10//! - Rich array/list functions
11
12use super::{DialectImpl, DialectType};
13use crate::error::Result;
14use crate::expressions::{
15    AggFunc, Alias, BinaryOp, Case, Cast, CeilFunc, Column, DataType, Expression, Function,
16    Identifier, Interval, IntervalUnit, IntervalUnitSpec, IsNull, JSONPath, JSONPathKey,
17    JSONPathRoot, JSONPathSubscript, JsonExtractFunc, Literal, Null, Paren, Struct, Subquery,
18    SubstringFunc, UnaryFunc, UnaryOp, Unhex, VarArgFunc, WindowFunction,
19};
20#[cfg(feature = "generate")]
21use crate::generator::GeneratorConfig;
22use crate::tokens::TokenizerConfig;
23
24/// Normalize a JSON path for DuckDB arrow syntax.
25/// Converts string keys like 'foo' to '$.foo' and numeric indexes like 0 to '$[0]'.
26/// This matches Python sqlglot's to_json_path() behavior.
27fn normalize_json_path(path: Expression) -> Expression {
28    match &path {
29        // String literal: 'foo' -> JSONPath with $.foo
30        Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
31            let Literal::String(s) = lit.as_ref() else {
32                unreachable!()
33            };
34            // Skip paths that are already normalized (start with $ or /)
35            // Also skip JSON pointer syntax and back-of-list syntax [#-i]
36            if s.starts_with('$') || s.starts_with('/') || s.contains("[#") {
37                return path;
38            }
39            // Create JSONPath expression: $.key
40            Expression::JSONPath(Box::new(JSONPath {
41                expressions: vec![
42                    Expression::JSONPathRoot(JSONPathRoot),
43                    Expression::JSONPathKey(Box::new(JSONPathKey {
44                        this: Box::new(Expression::Literal(Box::new(Literal::String(s.clone())))),
45                    })),
46                ],
47                escape: None,
48            }))
49        }
50        // Number literal: 0 -> JSONPath with $[0]
51        Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => {
52            let Literal::Number(n) = lit.as_ref() else {
53                unreachable!()
54            };
55            // Create JSONPath expression: $[n]
56            Expression::JSONPath(Box::new(JSONPath {
57                expressions: vec![
58                    Expression::JSONPathRoot(JSONPathRoot),
59                    Expression::JSONPathSubscript(Box::new(JSONPathSubscript {
60                        this: Box::new(Expression::Literal(Box::new(Literal::Number(n.clone())))),
61                    })),
62                ],
63                escape: None,
64            }))
65        }
66        // Already a JSONPath or other expression - return as is
67        _ => path,
68    }
69}
70
71/// Helper to wrap JSON arrow expressions in parentheses when they appear
72/// in contexts that require it (Binary, In, Not expressions)
73/// This matches Python sqlglot's WRAPPED_JSON_EXTRACT_EXPRESSIONS behavior
74fn wrap_if_json_arrow(expr: Expression) -> Expression {
75    match &expr {
76        Expression::JsonExtract(f) if f.arrow_syntax => Expression::Paren(Box::new(Paren {
77            this: expr,
78            trailing_comments: Vec::new(),
79        })),
80        Expression::JsonExtractScalar(f) if f.arrow_syntax => Expression::Paren(Box::new(Paren {
81            this: expr,
82            trailing_comments: Vec::new(),
83        })),
84        _ => expr,
85    }
86}
87
88/// DuckDB dialect
89pub struct DuckDBDialect;
90
91impl DialectImpl for DuckDBDialect {
92    fn dialect_type(&self) -> DialectType {
93        DialectType::DuckDB
94    }
95
96    fn tokenizer_config(&self) -> TokenizerConfig {
97        let mut config = TokenizerConfig::default();
98        // DuckDB uses double quotes for identifiers
99        config.identifiers.insert('"', '"');
100        // A leading escaped double quote belongs to the identifier, not a triple-quoted string.
101        config.quotes.remove("\"\"\"");
102        // DuckDB supports nested comments
103        config.nested_comments = true;
104        // DuckDB allows underscores as digit separators in numeric literals
105        config.numbers_can_be_underscore_separated = true;
106        // `//` is integer division (same tokenizer-level recognition as Vertica)
107        config.double_slash_int_div = true;
108        config
109    }
110
111    #[cfg(feature = "generate")]
112
113    fn generator_config(&self) -> GeneratorConfig {
114        use crate::generator::IdentifierQuoteStyle;
115        GeneratorConfig {
116            identifier_quote: '"',
117            identifier_quote_style: IdentifierQuoteStyle::DOUBLE_QUOTE,
118            dialect: Some(DialectType::DuckDB),
119            // DuckDB-specific settings from Python sqlglot
120            parameter_token: "$",
121            named_placeholder_token: "$",
122            join_hints: false,
123            table_hints: false,
124            query_hints: false,
125            limit_fetch_style: crate::generator::LimitFetchStyle::Limit,
126            struct_delimiter: ("(", ")"),
127            rename_table_with_db: false,
128            nvl2_supported: false,
129            semi_anti_join_with_side: false,
130            tablesample_keywords: "TABLESAMPLE",
131            tablesample_seed_keyword: "REPEATABLE",
132            last_day_supports_date_part: false,
133            json_key_value_pair_sep: ",",
134            ignore_nulls_in_func: true,
135            json_path_bracketed_key_supported: false,
136            supports_create_table_like: false,
137            multi_arg_distinct: false,
138            quantified_no_paren_space: false,
139            can_implement_array_any: true,
140            supports_to_number: false,
141            supports_window_exclude: true,
142            copy_has_into_keyword: false,
143            star_except: "EXCLUDE",
144            pad_fill_pattern_is_required: true,
145            array_concat_is_var_len: false,
146            array_size_dim_required: None,
147            normalize_extract_date_parts: true,
148            supports_like_quantifiers: false,
149            // DuckDB supports TRY_CAST
150            try_supported: true,
151            // DuckDB uses curly brace notation for struct literals: {'a': 1}
152            struct_curly_brace_notation: true,
153            // DuckDB uses bracket-only notation for arrays: [1, 2, 3]
154            array_bracket_only: true,
155            ..Default::default()
156        }
157    }
158
159    #[cfg(feature = "transpile")]
160
161    fn transform_expr(&self, expr: Expression) -> Result<Expression> {
162        match expr {
163            // ===== Data Type Mappings =====
164            Expression::DataType(dt) => self.transform_data_type(dt),
165
166            // ===== Operator transformations =====
167            // BitwiseXor -> XOR() function in DuckDB
168            Expression::BitwiseXor(op) => Ok(Expression::Function(Box::new(
169                crate::expressions::Function::new("XOR", vec![op.left, op.right]),
170            ))),
171
172            // ===== Array/List syntax =====
173            // ARRAY[1, 2, 3] -> [1, 2, 3] in DuckDB (bracket notation preferred)
174            Expression::ArrayFunc(mut f) => {
175                f.bracket_notation = true;
176                Ok(Expression::ArrayFunc(f))
177            }
178
179            // IFNULL -> COALESCE in DuckDB
180            Expression::IfNull(f) => Ok(Expression::Coalesce(Box::new(VarArgFunc {
181                original_name: None,
182                expressions: vec![f.this, f.expression],
183                inferred_type: None,
184            }))),
185
186            // NVL -> COALESCE in DuckDB
187            Expression::Nvl(f) => Ok(Expression::Coalesce(Box::new(VarArgFunc {
188                original_name: None,
189                expressions: vec![f.this, f.expression],
190                inferred_type: None,
191            }))),
192
193            // Coalesce with original_name (e.g., IFNULL parsed as Coalesce) -> clear original_name
194            Expression::Coalesce(mut f) => {
195                f.original_name = None;
196                Ok(Expression::Coalesce(f))
197            }
198
199            // GROUP_CONCAT -> LISTAGG in DuckDB
200            Expression::GroupConcat(f) => Ok(Expression::ListAgg(Box::new(
201                crate::expressions::ListAggFunc {
202                    source_dialect: None,
203                    this: f.this,
204                    separator: f.separator,
205                    on_overflow: None,
206                    max_length: None,
207                    order_by: f.order_by,
208                    distinct: f.distinct,
209                    filter: f.filter,
210                    inferred_type: None,
211                },
212            ))),
213
214            // LISTAGG is native in DuckDB - keep as-is
215            Expression::ListAgg(f) => Ok(Expression::ListAgg(f)),
216
217            // STRING_AGG -> LISTAGG in DuckDB (normalize to LISTAGG)
218            Expression::StringAgg(f) => Ok(Expression::ListAgg(Box::new(
219                crate::expressions::ListAggFunc {
220                    source_dialect: None,
221                    this: f.this,
222                    separator: f.separator,
223                    on_overflow: None,
224                    max_length: None,
225                    order_by: f.order_by,
226                    distinct: f.distinct,
227                    filter: f.filter,
228                    inferred_type: None,
229                },
230            ))),
231
232            // TryCast -> TRY_CAST (DuckDB supports TRY_CAST)
233            Expression::TryCast(c) => Ok(Expression::TryCast(c)),
234
235            // SafeCast -> TRY_CAST in DuckDB
236            Expression::SafeCast(c) => Ok(Expression::TryCast(c)),
237
238            // ILIKE is native to DuckDB (PostgreSQL-compatible)
239            Expression::ILike(op) => Ok(Expression::ILike(op)),
240
241            // EXPLODE -> UNNEST in DuckDB
242            Expression::Explode(f) => Ok(Expression::Unnest(Box::new(
243                crate::expressions::UnnestFunc {
244                    this: f.this,
245                    expressions: Vec::new(),
246                    with_ordinality: false,
247                    alias: None,
248                    offset_alias: None,
249                    inferred_type: None,
250                },
251            ))),
252
253            // UNNEST is native to DuckDB
254            Expression::Unnest(f) => Ok(Expression::Unnest(f)),
255
256            // DATE_ADD -> date + INTERVAL in DuckDB
257            Expression::DateAdd(f) => {
258                // Reconstruct INTERVAL expression from value and unit
259                let interval_expr = if matches!(&f.interval, Expression::Interval(_)) {
260                    f.interval
261                } else {
262                    Expression::Interval(Box::new(Interval {
263                        this: Some(f.interval),
264                        unit: Some(IntervalUnitSpec::Simple {
265                            unit: f.unit,
266                            use_plural: false,
267                        }),
268                    }))
269                };
270                Ok(Expression::Add(Box::new(BinaryOp {
271                    left: f.this,
272                    right: interval_expr,
273                    left_comments: Vec::new(),
274                    operator_comments: Vec::new(),
275                    trailing_comments: Vec::new(),
276                    inferred_type: None,
277                })))
278            }
279
280            // DATE_SUB -> date - INTERVAL in DuckDB
281            Expression::DateSub(f) => {
282                // Reconstruct INTERVAL expression from value and unit
283                let interval_expr = if matches!(&f.interval, Expression::Interval(_)) {
284                    f.interval
285                } else {
286                    Expression::Interval(Box::new(Interval {
287                        this: Some(f.interval),
288                        unit: Some(IntervalUnitSpec::Simple {
289                            unit: f.unit,
290                            use_plural: false,
291                        }),
292                    }))
293                };
294                Ok(Expression::Sub(Box::new(BinaryOp {
295                    left: f.this,
296                    right: interval_expr,
297                    left_comments: Vec::new(),
298                    operator_comments: Vec::new(),
299                    trailing_comments: Vec::new(),
300                    inferred_type: None,
301                })))
302            }
303
304            // GenerateSeries with 1 arg -> GENERATE_SERIES(0, n)
305            Expression::GenerateSeries(mut f) => {
306                // If only end is set (no start), add 0 as start
307                if f.start.is_none() && f.end.is_some() {
308                    f.start = Some(Box::new(Expression::number(0)));
309                }
310                Ok(Expression::GenerateSeries(f))
311            }
312
313            // ===== Array/List functions =====
314            // ArrayAppend -> LIST_APPEND
315            Expression::ArrayAppend(f) => Ok(Expression::Function(Box::new(Function::new(
316                "LIST_APPEND".to_string(),
317                vec![f.this, f.expression],
318            )))),
319
320            // ArrayPrepend -> LIST_PREPEND(element, array) - note arg swap
321            Expression::ArrayPrepend(f) => Ok(Expression::Function(Box::new(Function::new(
322                "LIST_PREPEND".to_string(),
323                vec![f.expression, f.this],
324            )))),
325
326            // ArrayUniqueAgg -> LIST(DISTINCT col) FILTER(WHERE NOT col IS NULL)
327            Expression::ArrayUniqueAgg(f) => {
328                let col = f.this;
329                // NOT col IS NULL
330                let filter_expr = Expression::Not(Box::new(UnaryOp {
331                    this: Expression::IsNull(Box::new(IsNull {
332                        this: col.clone(),
333                        not: false,
334                        postfix_form: false,
335                    })),
336                    inferred_type: None,
337                }));
338                Ok(Expression::ArrayAgg(Box::new(AggFunc {
339                    this: col,
340                    distinct: true,
341                    filter: Some(filter_expr),
342                    order_by: Vec::new(),
343                    name: Some("LIST".to_string()),
344                    ignore_nulls: None,
345                    having_max: None,
346                    limit: None,
347                    inferred_type: None,
348                })))
349            }
350
351            // Split -> STR_SPLIT
352            Expression::Split(f) => Ok(Expression::Function(Box::new(Function::new(
353                "STR_SPLIT".to_string(),
354                vec![f.this, f.delimiter],
355            )))),
356
357            // RANDOM is native to DuckDB
358            Expression::Random(_) => Ok(Expression::Random(crate::expressions::Random)),
359
360            // Rand with seed -> keep as Rand so NORMAL/UNIFORM handlers can extract the seed
361            // Rand without seed -> Random
362            Expression::Rand(r) => {
363                if r.seed.is_some() {
364                    Ok(Expression::Rand(r))
365                } else {
366                    Ok(Expression::Random(crate::expressions::Random))
367                }
368            }
369
370            // ===== Boolean aggregates =====
371            // LogicalAnd -> BOOL_AND with CAST to BOOLEAN
372            Expression::LogicalAnd(f) => Ok(Expression::Function(Box::new(Function::new(
373                "BOOL_AND".to_string(),
374                vec![Expression::Cast(Box::new(crate::expressions::Cast {
375                    this: f.this,
376                    to: crate::expressions::DataType::Boolean,
377                    trailing_comments: Vec::new(),
378                    double_colon_syntax: false,
379                    format: None,
380                    default: None,
381                    inferred_type: None,
382                }))],
383            )))),
384
385            // LogicalOr -> BOOL_OR with CAST to BOOLEAN
386            Expression::LogicalOr(f) => Ok(Expression::Function(Box::new(Function::new(
387                "BOOL_OR".to_string(),
388                vec![Expression::Cast(Box::new(crate::expressions::Cast {
389                    this: f.this,
390                    to: crate::expressions::DataType::Boolean,
391                    trailing_comments: Vec::new(),
392                    double_colon_syntax: false,
393                    format: None,
394                    default: None,
395                    inferred_type: None,
396                }))],
397            )))),
398
399            // ===== Approximate functions =====
400            // ApproxDistinct -> APPROX_COUNT_DISTINCT
401            Expression::ApproxDistinct(f) => Ok(Expression::Function(Box::new(Function::new(
402                "APPROX_COUNT_DISTINCT".to_string(),
403                vec![f.this],
404            )))),
405
406            // ===== Variance =====
407            // VarPop -> VAR_POP
408            Expression::VarPop(f) => Ok(Expression::Function(Box::new(Function::new(
409                "VAR_POP".to_string(),
410                vec![f.this],
411            )))),
412
413            // ===== Date/time functions =====
414            // DayOfMonth -> DAYOFMONTH
415            Expression::DayOfMonth(f) => Ok(Expression::Function(Box::new(Function::new(
416                "DAYOFMONTH".to_string(),
417                vec![f.this],
418            )))),
419
420            // DayOfWeek -> DAYOFWEEK
421            Expression::DayOfWeek(f) => Ok(Expression::Function(Box::new(Function::new(
422                "DAYOFWEEK".to_string(),
423                vec![f.this],
424            )))),
425
426            // DayOfWeekIso -> ISODOW
427            Expression::DayOfWeekIso(f) => Ok(Expression::Function(Box::new(Function::new(
428                "ISODOW".to_string(),
429                vec![f.this],
430            )))),
431
432            // DayOfYear -> DAYOFYEAR
433            Expression::DayOfYear(f) => Ok(Expression::Function(Box::new(Function::new(
434                "DAYOFYEAR".to_string(),
435                vec![f.this],
436            )))),
437
438            // WeekOfYear -> WEEKOFYEAR
439            Expression::WeekOfYear(f) => Ok(Expression::Function(Box::new(Function::new(
440                "WEEKOFYEAR".to_string(),
441                vec![f.this],
442            )))),
443
444            // ===== Time conversion functions =====
445            // TimeStrToUnix -> EPOCH
446            Expression::TimeStrToUnix(f) => Ok(Expression::Function(Box::new(Function::new(
447                "EPOCH".to_string(),
448                vec![f.this],
449            )))),
450
451            // TimeToUnix -> EPOCH
452            Expression::TimeToUnix(f) => Ok(Expression::Function(Box::new(Function::new(
453                "EPOCH".to_string(),
454                vec![f.this],
455            )))),
456
457            // UnixMicros -> EPOCH_US
458            Expression::UnixMicros(f) => Ok(Expression::Function(Box::new(Function::new(
459                "EPOCH_US".to_string(),
460                vec![f.this],
461            )))),
462
463            // UnixMillis -> EPOCH_MS
464            Expression::UnixMillis(f) => Ok(Expression::Function(Box::new(Function::new(
465                "EPOCH_MS".to_string(),
466                vec![f.this],
467            )))),
468
469            // TimestampDiff -> DATE_DIFF
470            Expression::TimestampDiff(f) => Ok(Expression::Function(Box::new(Function::new(
471                "DATE_DIFF".to_string(),
472                vec![*f.this, *f.expression],
473            )))),
474
475            // ===== Hash functions =====
476            // SHA -> SHA1
477            Expression::SHA(f) => Ok(Expression::Function(Box::new(Function::new(
478                "SHA1".to_string(),
479                vec![f.this],
480            )))),
481
482            // MD5Digest -> UNHEX(MD5(...))
483            Expression::MD5Digest(f) => Ok(Expression::Function(Box::new(Function::new(
484                "UNHEX".to_string(),
485                vec![*f.this],
486            )))),
487
488            // SHA1Digest -> UNHEX
489            Expression::SHA1Digest(f) => Ok(Expression::Function(Box::new(Function::new(
490                "UNHEX".to_string(),
491                vec![f.this],
492            )))),
493
494            // SHA2Digest -> UNHEX
495            Expression::SHA2Digest(f) => Ok(Expression::Function(Box::new(Function::new(
496                "UNHEX".to_string(),
497                vec![*f.this],
498            )))),
499
500            // ===== Vector/Distance functions =====
501            // CosineDistance -> LIST_COSINE_DISTANCE
502            Expression::CosineDistance(f) => Ok(Expression::Function(Box::new(Function::new(
503                "LIST_COSINE_DISTANCE".to_string(),
504                vec![*f.this, *f.expression],
505            )))),
506
507            // EuclideanDistance -> LIST_DISTANCE
508            Expression::EuclideanDistance(f) => Ok(Expression::Function(Box::new(Function::new(
509                "LIST_DISTANCE".to_string(),
510                vec![*f.this, *f.expression],
511            )))),
512
513            // ===== Numeric checks =====
514            // IsInf -> ISINF
515            Expression::IsInf(f) => Ok(Expression::Function(Box::new(Function::new(
516                "ISINF".to_string(),
517                vec![f.this],
518            )))),
519
520            // IsNan -> ISNAN
521            Expression::IsNan(f) => Ok(Expression::Function(Box::new(Function::new(
522                "ISNAN".to_string(),
523                vec![f.this],
524            )))),
525
526            // ===== Pattern matching =====
527            // RegexpLike (~) -> REGEXP_FULL_MATCH in DuckDB
528            Expression::RegexpLike(f) => Ok(Expression::Function(Box::new(Function::new(
529                "REGEXP_FULL_MATCH".to_string(),
530                vec![f.this, f.pattern],
531            )))),
532
533            // ===== Time functions =====
534            // CurrentTime -> CURRENT_TIME (no parens in DuckDB)
535            Expression::CurrentTime(_) => Ok(Expression::Function(Box::new(Function {
536                on_error: None,
537                qualified_name: Vec::new(),
538                source_dialect: None,
539
540                name: "CURRENT_TIME".to_string(),
541                args: vec![],
542                distinct: false,
543                trailing_comments: vec![],
544                use_bracket_syntax: false,
545                no_parens: true,
546                quoted: false,
547                span: None,
548                inferred_type: None,
549            }))),
550
551            // ===== Return statement =====
552            // ReturnStmt -> just output the inner expression
553            Expression::ReturnStmt(e) => Ok(*e),
554
555            // ===== DDL Column Constraints =====
556            // CommentColumnConstraint -> ignored (DuckDB doesn't support column comments this way)
557            Expression::CommentColumnConstraint(_) => Ok(Expression::Literal(Box::new(
558                crate::expressions::Literal::String(String::new()),
559            ))),
560
561            // JsonExtract -> use arrow syntax (->) in DuckDB with normalized JSON path
562            Expression::JsonExtract(mut f) => {
563                f.arrow_syntax = true;
564                f.path = normalize_json_path(f.path);
565                Ok(Expression::JsonExtract(f))
566            }
567
568            // JsonExtractScalar -> use arrow syntax (->>) in DuckDB with normalized JSON path
569            Expression::JsonExtractScalar(mut f) => {
570                f.arrow_syntax = true;
571                f.path = normalize_json_path(f.path);
572                Ok(Expression::JsonExtractScalar(f))
573            }
574
575            // CARDINALITY: keep as Expression::Cardinality - cross_dialect_normalize handles
576            // the conversion to target-specific form. For DuckDB->DuckDB, CARDINALITY is preserved
577            // (used for maps), for DuckDB->other it goes through ArrayLengthConvert.
578
579            // ADD_MONTHS(date, n) -> convert to Function and handle in transform_function
580            Expression::AddMonths(f) => {
581                let func = Function::new("ADD_MONTHS".to_string(), vec![f.this, f.expression]);
582                self.transform_function(func)
583            }
584
585            // NEXT_DAY(date, day) -> convert to Function and handle in transform_function
586            Expression::NextDay(f) => {
587                let func = Function::new("NEXT_DAY".to_string(), vec![f.this, f.expression]);
588                self.transform_function(func)
589            }
590
591            // LAST_DAY(date, unit) -> convert to Function and handle in transform_function
592            Expression::LastDay(f) => {
593                if let Some(unit) = f.unit {
594                    let unit_str = match unit {
595                        crate::expressions::DateTimeField::Year => "YEAR",
596                        crate::expressions::DateTimeField::Month => "MONTH",
597                        crate::expressions::DateTimeField::Quarter => "QUARTER",
598                        crate::expressions::DateTimeField::Week => "WEEK",
599                        crate::expressions::DateTimeField::Day => "DAY",
600                        _ => "MONTH",
601                    };
602                    let func = Function::new(
603                        "LAST_DAY".to_string(),
604                        vec![
605                            f.this,
606                            Expression::Identifier(Identifier {
607                                name: unit_str.to_string(),
608                                quoted: false,
609                                trailing_comments: Vec::new(),
610                                span: None,
611                            }),
612                        ],
613                    );
614                    self.transform_function(func)
615                } else {
616                    // Single arg LAST_DAY - pass through
617                    Ok(Expression::Function(Box::new(Function::new(
618                        "LAST_DAY".to_string(),
619                        vec![f.this],
620                    ))))
621                }
622            }
623
624            // DAYNAME(expr) -> STRFTIME(expr, '%a')
625            Expression::Dayname(d) => Ok(Expression::Function(Box::new(Function::new(
626                "STRFTIME".to_string(),
627                vec![
628                    *d.this,
629                    Expression::Literal(Box::new(Literal::String("%a".to_string()))),
630                ],
631            )))),
632
633            // MONTHNAME(expr) -> STRFTIME(expr, '%b')
634            Expression::Monthname(d) => Ok(Expression::Function(Box::new(Function::new(
635                "STRFTIME".to_string(),
636                vec![
637                    *d.this,
638                    Expression::Literal(Box::new(Literal::String("%b".to_string()))),
639                ],
640            )))),
641
642            // FLOOR(x, scale) -> ROUND(FLOOR(x * POWER(10, scale)) / POWER(10, scale), scale)
643            Expression::Floor(f) if f.scale.is_some() => {
644                let x = f.this;
645                let scale = f.scale.unwrap();
646                let needs_cast = match &scale {
647                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => {
648                        let Literal::Number(n) = lit.as_ref() else {
649                            unreachable!()
650                        };
651                        n.contains('.')
652                    }
653                    _ => false,
654                };
655                let int_scale = if needs_cast {
656                    Expression::Cast(Box::new(Cast {
657                        this: scale.clone(),
658                        to: DataType::Int {
659                            length: None,
660                            integer_spelling: false,
661                        },
662                        trailing_comments: Vec::new(),
663                        double_colon_syntax: false,
664                        format: None,
665                        default: None,
666                        inferred_type: None,
667                    }))
668                } else {
669                    scale.clone()
670                };
671                let power_10 = Expression::Function(Box::new(Function::new(
672                    "POWER".to_string(),
673                    vec![Expression::number(10), int_scale.clone()],
674                )));
675                let x_paren = match &x {
676                    Expression::Add(_)
677                    | Expression::Sub(_)
678                    | Expression::Mul(_)
679                    | Expression::Div(_) => Expression::Paren(Box::new(Paren {
680                        this: x,
681                        trailing_comments: Vec::new(),
682                    })),
683                    _ => x,
684                };
685                let multiplied = Expression::Mul(Box::new(BinaryOp {
686                    left: x_paren,
687                    right: power_10.clone(),
688                    left_comments: Vec::new(),
689                    operator_comments: Vec::new(),
690                    trailing_comments: Vec::new(),
691                    inferred_type: None,
692                }));
693                let floored = Expression::Function(Box::new(Function::new(
694                    "FLOOR".to_string(),
695                    vec![multiplied],
696                )));
697                let divided = Expression::Div(Box::new(BinaryOp {
698                    left: floored,
699                    right: power_10,
700                    left_comments: Vec::new(),
701                    operator_comments: Vec::new(),
702                    trailing_comments: Vec::new(),
703                    inferred_type: None,
704                }));
705                Ok(Expression::Function(Box::new(Function::new(
706                    "ROUND".to_string(),
707                    vec![divided, int_scale],
708                ))))
709            }
710
711            // CEIL(x, scale) -> ROUND(CEIL(x * POWER(10, scale)) / POWER(10, scale), scale)
712            Expression::Ceil(f) if f.decimals.is_some() => {
713                let x = f.this;
714                let scale = f.decimals.unwrap();
715                let needs_cast = match &scale {
716                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => {
717                        let Literal::Number(n) = lit.as_ref() else {
718                            unreachable!()
719                        };
720                        n.contains('.')
721                    }
722                    _ => false,
723                };
724                let int_scale = if needs_cast {
725                    Expression::Cast(Box::new(Cast {
726                        this: scale.clone(),
727                        to: DataType::Int {
728                            length: None,
729                            integer_spelling: false,
730                        },
731                        trailing_comments: Vec::new(),
732                        double_colon_syntax: false,
733                        format: None,
734                        default: None,
735                        inferred_type: None,
736                    }))
737                } else {
738                    scale.clone()
739                };
740                let power_10 = Expression::Function(Box::new(Function::new(
741                    "POWER".to_string(),
742                    vec![Expression::number(10), int_scale.clone()],
743                )));
744                let x_paren = match &x {
745                    Expression::Add(_)
746                    | Expression::Sub(_)
747                    | Expression::Mul(_)
748                    | Expression::Div(_) => Expression::Paren(Box::new(Paren {
749                        this: x,
750                        trailing_comments: Vec::new(),
751                    })),
752                    _ => x,
753                };
754                let multiplied = Expression::Mul(Box::new(BinaryOp {
755                    left: x_paren,
756                    right: power_10.clone(),
757                    left_comments: Vec::new(),
758                    operator_comments: Vec::new(),
759                    trailing_comments: Vec::new(),
760                    inferred_type: None,
761                }));
762                let ceiled = Expression::Function(Box::new(Function::new(
763                    "CEIL".to_string(),
764                    vec![multiplied],
765                )));
766                let divided = Expression::Div(Box::new(BinaryOp {
767                    left: ceiled,
768                    right: power_10,
769                    left_comments: Vec::new(),
770                    operator_comments: Vec::new(),
771                    trailing_comments: Vec::new(),
772                    inferred_type: None,
773                }));
774                Ok(Expression::Function(Box::new(Function::new(
775                    "ROUND".to_string(),
776                    vec![divided, int_scale],
777                ))))
778            }
779
780            // ParseJson: handled by generator (outputs JSON() for DuckDB)
781
782            // TABLE(GENERATOR(ROWCOUNT => n)) -> RANGE(n) in DuckDB
783            // The TABLE() wrapper around GENERATOR is parsed as TableArgument
784            Expression::TableArgument(ta) if ta.prefix.to_uppercase() == "TABLE" => {
785                // Check if inner is a GENERATOR or RANGE function
786                match ta.this {
787                    Expression::Function(ref f) if f.name.to_uppercase() == "RANGE" => {
788                        // Already converted to RANGE, unwrap TABLE()
789                        Ok(ta.this)
790                    }
791                    Expression::Function(ref f) if f.name.to_uppercase() == "GENERATOR" => {
792                        // GENERATOR(ROWCOUNT => n) -> RANGE(n)
793                        let mut rowcount = None;
794                        for arg in &f.args {
795                            if let Expression::NamedArgument(na) = arg {
796                                if na.name.name.to_uppercase() == "ROWCOUNT" {
797                                    rowcount = Some(na.value.clone());
798                                }
799                            }
800                        }
801                        if let Some(n) = rowcount {
802                            Ok(Expression::Function(Box::new(Function::new(
803                                "RANGE".to_string(),
804                                vec![n],
805                            ))))
806                        } else {
807                            Ok(Expression::TableArgument(ta))
808                        }
809                    }
810                    _ => Ok(Expression::TableArgument(ta)),
811                }
812            }
813
814            // JSONExtract (variant_extract/colon accessor) -> arrow syntax in DuckDB
815            Expression::JSONExtract(e) if e.variant_extract.is_some() => {
816                let path = match *e.expression {
817                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
818                        let Literal::String(s) = lit.as_ref() else {
819                            unreachable!()
820                        };
821                        // Convert bracket notation ["key"] to quoted dot notation ."key"
822                        let s = Self::convert_bracket_to_quoted_path(&s);
823                        let normalized = if s.starts_with('$') {
824                            s
825                        } else if s.starts_with('[') {
826                            format!("${}", s)
827                        } else {
828                            format!("$.{}", s)
829                        };
830                        Expression::Literal(Box::new(Literal::String(normalized)))
831                    }
832                    other => other,
833                };
834                Ok(Expression::JsonExtract(Box::new(JsonExtractFunc {
835                    options: None,
836                    source_dialect: None,
837
838                    this: *e.this,
839                    path,
840                    returning: None,
841                    arrow_syntax: true,
842                    hash_arrow_syntax: false,
843                    wrapper_option: None,
844                    quotes_option: None,
845                    on_scalar_string: false,
846                    on_error: None,
847                })))
848            }
849
850            // X'ABCD' -> UNHEX('ABCD') in DuckDB
851            Expression::Literal(lit) if matches!(lit.as_ref(), Literal::HexString(_)) => {
852                let Literal::HexString(s) = lit.as_ref() else {
853                    unreachable!()
854                };
855                Ok(Expression::Function(Box::new(Function::new(
856                    "UNHEX".to_string(),
857                    vec![Expression::Literal(Box::new(Literal::String(s.clone())))],
858                ))))
859            }
860
861            // b'a' -> CAST(e'a' AS BLOB) in DuckDB
862            Expression::Literal(lit) if matches!(lit.as_ref(), Literal::ByteString(_)) => {
863                let Literal::ByteString(s) = lit.as_ref() else {
864                    unreachable!()
865                };
866                Ok(Expression::Cast(Box::new(Cast {
867                    this: Expression::Literal(Box::new(Literal::EscapeString(s.clone()))),
868                    to: DataType::VarBinary { length: None },
869                    trailing_comments: Vec::new(),
870                    double_colon_syntax: false,
871                    format: None,
872                    default: None,
873                    inferred_type: None,
874                })))
875            }
876
877            // Keep omitted decimal parameters until generation, where the source
878            // dialect is available to distinguish DuckDB and BigQuery defaults.
879            Expression::Cast(mut c) => {
880                let transformed_this = self.transform_expr(c.this)?;
881                c.this = transformed_this;
882                Ok(Expression::Cast(c))
883            }
884
885            // Generic function transformations
886            Expression::Function(f) => self.transform_function(*f),
887
888            // Generic aggregate function transformations
889            Expression::AggregateFunction(f) => self.transform_aggregate_function(f),
890
891            // WindowFunction with CASE-wrapped CORR: re-wrap so OVER is inside CASE
892            // Pattern: WindowFunction { this: CASE(ISNAN(CORR), NULL, CORR), over }
893            // Expected: CASE(ISNAN(WindowFunction(CORR, over)), NULL, WindowFunction(CORR, over))
894            Expression::WindowFunction(wf) => {
895                if let Expression::Case(case_box) = wf.this {
896                    let case = *case_box;
897                    // Detect the ISNAN(CORR) -> NULL pattern
898                    if case.whens.len() == 1
899                        && matches!(&case.else_, Some(Expression::AggregateFunction(ref af)) if af.name.to_uppercase() == "CORR")
900                    {
901                        // Re-wrap: put the OVER on each CORR inside the CASE
902                        let over = wf.over;
903                        let new_else = case.else_.map(|e| {
904                            Expression::WindowFunction(Box::new(WindowFunction {
905                                this: e,
906                                over: over.clone(),
907                                keep: None,
908                                inferred_type: None,
909                            }))
910                        });
911                        let new_whens = case
912                            .whens
913                            .into_iter()
914                            .map(|(when_cond, when_result)| {
915                                // wrap the ISNAN arg (which is CORR) with OVER
916                                let new_cond = if let Expression::Function(func) = when_cond {
917                                    if func.name.to_uppercase() == "ISNAN" && func.args.len() == 1 {
918                                        let inner = func.args.into_iter().next().unwrap();
919                                        let windowed =
920                                            Expression::WindowFunction(Box::new(WindowFunction {
921                                                this: inner,
922                                                over: over.clone(),
923                                                keep: None,
924                                                inferred_type: None,
925                                            }));
926                                        Expression::Function(Box::new(Function::new(
927                                            "ISNAN".to_string(),
928                                            vec![windowed],
929                                        )))
930                                    } else {
931                                        Expression::Function(func)
932                                    }
933                                } else {
934                                    when_cond
935                                };
936                                (new_cond, when_result)
937                            })
938                            .collect();
939                        Ok(Expression::Case(Box::new(Case {
940                            operand: None,
941                            whens: new_whens,
942                            else_: new_else,
943                            comments: Vec::new(),
944                            inferred_type: None,
945                        })))
946                    } else {
947                        Ok(Expression::WindowFunction(Box::new(WindowFunction {
948                            this: Expression::Case(Box::new(case)),
949                            over: wf.over,
950                            keep: wf.keep,
951                            inferred_type: None,
952                        })))
953                    }
954                } else {
955                    Ok(Expression::WindowFunction(wf))
956                }
957            }
958
959            // ===== Context-aware JSON arrow wrapping =====
960            // When JSON arrow expressions appear in Binary/In/Not contexts,
961            // they need to be wrapped in parentheses for correct precedence.
962            // This matches Python sqlglot's WRAPPED_JSON_EXTRACT_EXPRESSIONS behavior.
963
964            // Binary operators that need JSON wrapping
965            Expression::Eq(op) => Ok(Expression::Eq(Box::new(BinaryOp {
966                left: wrap_if_json_arrow(op.left),
967                right: wrap_if_json_arrow(op.right),
968                ..*op
969            }))),
970            Expression::Neq(op) => Ok(Expression::Neq(Box::new(BinaryOp {
971                left: wrap_if_json_arrow(op.left),
972                right: wrap_if_json_arrow(op.right),
973                ..*op
974            }))),
975            Expression::Lt(op) => Ok(Expression::Lt(Box::new(BinaryOp {
976                left: wrap_if_json_arrow(op.left),
977                right: wrap_if_json_arrow(op.right),
978                ..*op
979            }))),
980            Expression::Lte(op) => Ok(Expression::Lte(Box::new(BinaryOp {
981                left: wrap_if_json_arrow(op.left),
982                right: wrap_if_json_arrow(op.right),
983                ..*op
984            }))),
985            Expression::Gt(op) => Ok(Expression::Gt(Box::new(BinaryOp {
986                left: wrap_if_json_arrow(op.left),
987                right: wrap_if_json_arrow(op.right),
988                ..*op
989            }))),
990            Expression::Gte(op) => Ok(Expression::Gte(Box::new(BinaryOp {
991                left: wrap_if_json_arrow(op.left),
992                right: wrap_if_json_arrow(op.right),
993                ..*op
994            }))),
995            Expression::And(op) => Ok(Expression::And(Box::new(BinaryOp {
996                left: wrap_if_json_arrow(op.left),
997                right: wrap_if_json_arrow(op.right),
998                ..*op
999            }))),
1000            Expression::Or(op) => Ok(Expression::Or(Box::new(BinaryOp {
1001                left: wrap_if_json_arrow(op.left),
1002                right: wrap_if_json_arrow(op.right),
1003                ..*op
1004            }))),
1005            Expression::Add(op) => Ok(Expression::Add(Box::new(BinaryOp {
1006                left: wrap_if_json_arrow(op.left),
1007                right: wrap_if_json_arrow(op.right),
1008                ..*op
1009            }))),
1010            Expression::Sub(op) => Ok(Expression::Sub(Box::new(BinaryOp {
1011                left: wrap_if_json_arrow(op.left),
1012                right: wrap_if_json_arrow(op.right),
1013                ..*op
1014            }))),
1015            Expression::Mul(op) => Ok(Expression::Mul(Box::new(BinaryOp {
1016                left: wrap_if_json_arrow(op.left),
1017                right: wrap_if_json_arrow(op.right),
1018                ..*op
1019            }))),
1020            Expression::Div(op) => Ok(Expression::Div(Box::new(BinaryOp {
1021                left: wrap_if_json_arrow(op.left),
1022                right: wrap_if_json_arrow(op.right),
1023                ..*op
1024            }))),
1025            Expression::Mod(op) => Ok(Expression::Mod(Box::new(BinaryOp {
1026                left: wrap_if_json_arrow(op.left),
1027                right: wrap_if_json_arrow(op.right),
1028                ..*op
1029            }))),
1030            Expression::Concat(op) => Ok(Expression::Concat(Box::new(BinaryOp {
1031                left: wrap_if_json_arrow(op.left),
1032                right: wrap_if_json_arrow(op.right),
1033                ..*op
1034            }))),
1035
1036            // In expression - wrap the this part if it's JSON arrow
1037            // Also transform `expr NOT IN (list)` to `NOT (expr) IN (list)` for DuckDB
1038            Expression::In(mut i) => {
1039                i.this = wrap_if_json_arrow(i.this);
1040                if i.not {
1041                    // Transform `expr NOT IN (list)` to `NOT (expr) IN (list)`
1042                    i.not = false;
1043                    Ok(Expression::Not(Box::new(crate::expressions::UnaryOp {
1044                        this: Expression::In(i),
1045                        inferred_type: None,
1046                    })))
1047                } else {
1048                    Ok(Expression::In(i))
1049                }
1050            }
1051
1052            // Not expression - wrap the this part if it's JSON arrow
1053            Expression::Not(mut n) => {
1054                n.this = wrap_if_json_arrow(n.this);
1055                Ok(Expression::Not(n))
1056            }
1057
1058            // WithinGroup: PERCENTILE_CONT/DISC WITHIN GROUP (ORDER BY ...) -> QUANTILE_CONT/DISC(col, quantile ORDER BY ...)
1059            Expression::WithinGroup(wg) => {
1060                match &wg.this {
1061                    Expression::ListAgg(listagg) => {
1062                        let mut listagg = listagg.clone();
1063                        listagg.order_by = Some(wg.order_by.clone());
1064                        Ok(Expression::ListAgg(listagg))
1065                    }
1066                    Expression::PercentileCont(p) => {
1067                        let column = wg
1068                            .order_by
1069                            .first()
1070                            .map(|o| o.this.clone())
1071                            .unwrap_or_else(|| p.this.clone());
1072                        let percentile = p.percentile.clone();
1073                        let filter = p.filter.clone();
1074                        Ok(Expression::AggregateFunction(Box::new(
1075                            crate::expressions::AggregateFunction {
1076                                source_dialect: None,
1077
1078                                name: "QUANTILE_CONT".to_string(),
1079                                args: vec![column, percentile],
1080                                distinct: false,
1081                                filter,
1082                                order_by: wg.order_by,
1083                                limit: None,
1084                                ignore_nulls: None,
1085                                inferred_type: None,
1086                            },
1087                        )))
1088                    }
1089                    Expression::PercentileDisc(p) => {
1090                        let column = wg
1091                            .order_by
1092                            .first()
1093                            .map(|o| o.this.clone())
1094                            .unwrap_or_else(|| p.this.clone());
1095                        let percentile = p.percentile.clone();
1096                        let filter = p.filter.clone();
1097                        Ok(Expression::AggregateFunction(Box::new(
1098                            crate::expressions::AggregateFunction {
1099                                source_dialect: None,
1100
1101                                name: "QUANTILE_DISC".to_string(),
1102                                args: vec![column, percentile],
1103                                distinct: false,
1104                                filter,
1105                                order_by: wg.order_by,
1106                                limit: None,
1107                                ignore_nulls: None,
1108                                inferred_type: None,
1109                            },
1110                        )))
1111                    }
1112                    // Handle case where inner is AggregateFunction with PERCENTILE_CONT/DISC name
1113                    Expression::AggregateFunction(af)
1114                        if af.name == "PERCENTILE_CONT" || af.name == "PERCENTILE_DISC" =>
1115                    {
1116                        let new_name = if af.name == "PERCENTILE_CONT" {
1117                            "QUANTILE_CONT"
1118                        } else {
1119                            "QUANTILE_DISC"
1120                        };
1121                        let column = wg.order_by.first().map(|o| o.this.clone());
1122                        let quantile = af.args.first().cloned();
1123                        match (column, quantile) {
1124                            (Some(col), Some(q)) => Ok(Expression::AggregateFunction(Box::new(
1125                                crate::expressions::AggregateFunction {
1126                                    source_dialect: None,
1127
1128                                    name: new_name.to_string(),
1129                                    args: vec![col, q],
1130                                    distinct: false,
1131                                    filter: af.filter.clone(),
1132                                    order_by: wg.order_by,
1133                                    limit: None,
1134                                    ignore_nulls: None,
1135                                    inferred_type: None,
1136                                },
1137                            ))),
1138                            _ => Ok(Expression::WithinGroup(wg)),
1139                        }
1140                    }
1141                    _ => Ok(Expression::WithinGroup(wg)),
1142                }
1143            }
1144
1145            // ===== DuckDB @ prefix operator → ABS() =====
1146            // In DuckDB, @expr means ABS(expr)
1147            // Parser creates Column with name "@col" — strip the @ and wrap in ABS()
1148            Expression::Column(ref c) if c.name.name.starts_with('@') && c.table.is_none() => {
1149                let col_name = &c.name.name[1..]; // strip leading @
1150                Ok(Expression::Abs(Box::new(UnaryFunc {
1151                    this: Expression::boxed_column(Column {
1152                        name: Identifier::new(col_name),
1153                        table: None,
1154                        join_mark: false,
1155                        trailing_comments: Vec::new(),
1156                        span: None,
1157                        inferred_type: None,
1158                    }),
1159                    original_name: None,
1160                    inferred_type: None,
1161                })))
1162            }
1163
1164            // ===== SELECT-level transforms =====
1165            // DuckDB colon alias syntax: `foo: bar` → `bar AS foo`
1166            // Parser creates JSONExtract(this=foo, expression='bar', variant_extract=true)
1167            // which needs to become Alias(this=Column(bar), alias=foo)
1168            Expression::Select(mut select) => {
1169                select.expressions = select
1170                    .expressions
1171                    .into_iter()
1172                    .map(|e| {
1173                        match e {
1174                            Expression::JSONExtract(ref je) if je.variant_extract.is_some() => {
1175                                // JSONExtract(this=alias_name, expression='value', variant_extract=true) → value AS alias_name
1176                                let alias_ident = match je.this.as_ref() {
1177                                    Expression::Identifier(ident) => Some(ident.clone()),
1178                                    Expression::Column(col) if col.table.is_none() => {
1179                                        Some(col.name.clone())
1180                                    }
1181                                    _ => None,
1182                                };
1183                                let value_expr = match je.expression.as_ref() {
1184                                    Expression::Literal(lit)
1185                                        if matches!(lit.as_ref(), Literal::String(_)) =>
1186                                    {
1187                                        let Literal::String(s) = lit.as_ref() else {
1188                                            unreachable!()
1189                                        };
1190                                        // Convert string path to column reference
1191                                        if s.contains('.') {
1192                                            // t.col → Column { name: col, table: t }
1193                                            let parts: Vec<&str> = s.splitn(2, '.').collect();
1194                                            Some(Expression::boxed_column(Column {
1195                                                name: Identifier::new(parts[1]),
1196                                                table: Some(Identifier::new(parts[0])),
1197                                                join_mark: false,
1198                                                trailing_comments: Vec::new(),
1199                                                span: None,
1200                                                inferred_type: None,
1201                                            }))
1202                                        } else {
1203                                            Some(Expression::boxed_column(Column {
1204                                                name: Identifier::new(s.as_str()),
1205                                                table: None,
1206                                                join_mark: false,
1207                                                trailing_comments: Vec::new(),
1208                                                span: None,
1209                                                inferred_type: None,
1210                                            }))
1211                                        }
1212                                    }
1213                                    _ => None,
1214                                };
1215
1216                                if let (Some(alias), Some(value)) = (alias_ident, value_expr) {
1217                                    Expression::Alias(Box::new(Alias {
1218                                        this: value,
1219                                        alias,
1220                                        column_aliases: Vec::new(),
1221                                        alias_explicit_as: false,
1222                                        alias_keyword: None,
1223                                        pre_alias_comments: Vec::new(),
1224                                        trailing_comments: Vec::new(),
1225                                        inferred_type: None,
1226                                    }))
1227                                } else {
1228                                    e
1229                                }
1230                            }
1231                            _ => e,
1232                        }
1233                    })
1234                    .collect();
1235
1236                // ===== DuckDB comma-join with UNNEST → JOIN ON TRUE =====
1237                // Transform FROM t1, UNNEST(...) AS t2 → FROM t1 JOIN UNNEST(...) AS t2 ON TRUE
1238                if let Some(ref mut from) = select.from {
1239                    if from.expressions.len() > 1 {
1240                        // Check if any expression after the first is UNNEST or Alias wrapping UNNEST
1241                        let mut new_from_exprs = Vec::new();
1242                        let mut new_joins = Vec::new();
1243
1244                        for (idx, expr) in from.expressions.drain(..).enumerate() {
1245                            if idx == 0 {
1246                                // First expression stays in FROM
1247                                new_from_exprs.push(expr);
1248                            } else {
1249                                // Check if this is UNNEST or Alias(UNNEST)
1250                                let is_unnest = match &expr {
1251                                    Expression::Unnest(_) => true,
1252                                    Expression::Alias(a) => matches!(a.this, Expression::Unnest(_)),
1253                                    _ => false,
1254                                };
1255
1256                                if is_unnest {
1257                                    // Convert to JOIN ON TRUE
1258                                    new_joins.push(crate::expressions::Join {
1259                                        this: expr,
1260                                        on: Some(Expression::Boolean(
1261                                            crate::expressions::BooleanLiteral { value: true },
1262                                        )),
1263                                        using: Vec::new(),
1264                                        kind: crate::expressions::JoinKind::Inner,
1265                                        use_inner_keyword: false,
1266                                        use_outer_keyword: false,
1267                                        deferred_condition: false,
1268                                        join_hint: None,
1269                                        match_condition: None,
1270                                        pivots: Vec::new(),
1271                                        comments: Vec::new(),
1272                                        nesting_group: 0,
1273                                        directed: false,
1274                                    });
1275                                } else {
1276                                    // Keep non-UNNEST expressions in FROM (comma-separated)
1277                                    new_from_exprs.push(expr);
1278                                }
1279                            }
1280                        }
1281
1282                        from.expressions = new_from_exprs;
1283
1284                        // Prepend the new joins before any existing joins
1285                        new_joins.append(&mut select.joins);
1286                        select.joins = new_joins;
1287                    }
1288                }
1289
1290                Ok(Expression::Select(select))
1291            }
1292
1293            // ===== INTERVAL splitting =====
1294            // DuckDB requires INTERVAL '1' HOUR format, not INTERVAL '1 hour'
1295            // When we have INTERVAL 'value unit' (single string with embedded unit),
1296            // split it into INTERVAL 'value' UNIT
1297            Expression::Interval(interval) => self.transform_interval(*interval),
1298
1299            // DuckDB CREATE FUNCTION (macro syntax): normalize param types
1300            Expression::CreateFunction(mut cf) => {
1301                // Apply DuckDB type normalization to function parameters (e.g., FLOAT -> REAL)
1302                cf.parameters = cf
1303                    .parameters
1304                    .into_iter()
1305                    .map(|mut p| {
1306                        if let Ok(Expression::DataType(new_dt)) =
1307                            self.transform_data_type(p.data_type.clone())
1308                        {
1309                            p.data_type = new_dt;
1310                        }
1311                        p
1312                    })
1313                    .collect();
1314
1315                // Normalize TABLE return: if returns_table_body is set (from other dialects
1316                // like TSQL/Databricks), convert to return_type = Custom { "TABLE" } marker.
1317                // This is dialect-agnostic and the generator handles DuckDB vs non-DuckDB output.
1318                if cf.returns_table_body.is_some()
1319                    && !matches!(&cf.return_type, Some(DataType::Custom { ref name }) if name == "TABLE")
1320                {
1321                    cf.return_type = Some(DataType::Custom {
1322                        name: "TABLE".to_string(),
1323                    });
1324                    cf.returns_table_body = None;
1325                }
1326
1327                Ok(Expression::CreateFunction(cf))
1328            }
1329
1330            // ===== Snowflake-specific expression type transforms =====
1331
1332            // IFF(cond, true_val, false_val) -> CASE WHEN cond THEN true_val ELSE false_val END
1333            Expression::IfFunc(f) => Ok(Expression::Case(Box::new(Case {
1334                operand: None,
1335                whens: vec![(f.condition, f.true_value)],
1336                else_: f.false_value,
1337                comments: Vec::new(),
1338                inferred_type: None,
1339            }))),
1340
1341            // VAR_SAMP -> VARIANCE in DuckDB
1342            Expression::VarSamp(f) => Ok(Expression::Function(Box::new(Function::new(
1343                "VARIANCE".to_string(),
1344                vec![f.this],
1345            )))),
1346
1347            // NVL2(expr, val_if_not_null, val_if_null) -> CASE WHEN expr IS NOT NULL THEN val_if_not_null ELSE val_if_null END
1348            Expression::Nvl2(f) => {
1349                let condition = Expression::IsNull(Box::new(crate::expressions::IsNull {
1350                    this: f.this,
1351                    not: true,
1352                    postfix_form: false,
1353                }));
1354                Ok(Expression::Case(Box::new(Case {
1355                    operand: None,
1356                    whens: vec![(condition, f.true_value)],
1357                    else_: Some(f.false_value),
1358                    comments: Vec::new(),
1359                    inferred_type: None,
1360                })))
1361            }
1362
1363            // Pass through everything else
1364            _ => Ok(expr),
1365        }
1366    }
1367}
1368
1369#[cfg(feature = "transpile")]
1370impl DuckDBDialect {
1371    /// Extract a numeric value from a literal expression, if possible
1372    fn extract_number_value(expr: &Expression) -> Option<f64> {
1373        match expr {
1374            Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => {
1375                let Literal::Number(n) = lit.as_ref() else {
1376                    unreachable!()
1377                };
1378                n.parse::<f64>().ok()
1379            }
1380            _ => None,
1381        }
1382    }
1383
1384    /// Convert an expression to a SQL string for template-based transformations
1385    fn expr_to_sql(expr: &Expression) -> String {
1386        crate::generator::Generator::sql(expr).unwrap_or_default()
1387    }
1388
1389    /// Extract the seed expression for random-based function emulations.
1390    /// Returns (seed_sql, is_random_no_seed) where:
1391    /// - For RANDOM(): ("RANDOM()", true)
1392    /// - For RANDOM(seed): ("seed", false) - extracts the seed
1393    /// - For literal seed: ("seed_value", false)
1394    fn extract_seed_info(gen: &Expression) -> (String, bool) {
1395        match gen {
1396            Expression::Function(func) if func.name.to_uppercase() == "RANDOM" => {
1397                if func.args.is_empty() {
1398                    ("RANDOM()".to_string(), true)
1399                } else {
1400                    // RANDOM(seed) -> extract the seed
1401                    (Self::expr_to_sql(&func.args[0]), false)
1402                }
1403            }
1404            Expression::Rand(r) => {
1405                if let Some(ref seed) = r.seed {
1406                    // RANDOM(seed) / RAND(seed) -> extract the seed
1407                    (Self::expr_to_sql(seed), false)
1408                } else {
1409                    ("RANDOM()".to_string(), true)
1410                }
1411            }
1412            Expression::Random(_) => ("RANDOM()".to_string(), true),
1413            _ => (Self::expr_to_sql(gen), false),
1414        }
1415    }
1416
1417    /// Parse a SQL template string and wrap it in a Subquery (parenthesized expression).
1418    /// Uses a thread with larger stack to handle deeply nested template SQL in debug builds.
1419    fn parse_as_subquery(sql: &str) -> Result<Expression> {
1420        let sql_owned = sql.to_string();
1421        let handle = std::thread::Builder::new()
1422            .stack_size(16 * 1024 * 1024) // 16MB stack for complex templates
1423            .spawn(move || match crate::parser::Parser::parse_sql(&sql_owned) {
1424                Ok(stmts) => {
1425                    if let Some(stmt) = stmts.into_iter().next() {
1426                        Ok(Expression::Subquery(Box::new(Subquery {
1427                            this: stmt,
1428                            alias: None,
1429                            column_aliases: Vec::new(),
1430                            alias_explicit_as: false,
1431                            alias_keyword: None,
1432                            order_by: None,
1433                            limit: None,
1434                            offset: None,
1435                            distribute_by: None,
1436                            sort_by: None,
1437                            cluster_by: None,
1438                            lateral: false,
1439                            modifiers_inside: false,
1440                            trailing_comments: Vec::new(),
1441                            inferred_type: None,
1442                        })))
1443                    } else {
1444                        Err(crate::error::Error::Generate(
1445                            "Failed to parse template SQL".to_string(),
1446                        ))
1447                    }
1448                }
1449                Err(e) => Err(e),
1450            })
1451            .map_err(|e| {
1452                crate::error::Error::Internal(format!("Failed to spawn parser thread: {}", e))
1453            })?;
1454
1455        handle
1456            .join()
1457            .map_err(|_| crate::error::Error::Internal("Parser thread panicked".to_string()))?
1458    }
1459
1460    /// Normalize CAST({} AS MAP(...)) style expressions to CAST(MAP() AS MAP(...)).
1461    fn normalize_empty_map_expr(expr: Expression) -> Expression {
1462        match expr {
1463            Expression::Cast(mut c) if matches!(&c.to, DataType::Map { .. }) => {
1464                if matches!(&c.this, Expression::Struct(s) if s.fields.is_empty()) {
1465                    c.this =
1466                        Expression::Function(Box::new(Function::new("MAP".to_string(), vec![])));
1467                }
1468                Expression::Cast(c)
1469            }
1470            other => other,
1471        }
1472    }
1473
1474    /// Convert bracket notation ["key with spaces"] to quoted dot notation ."key with spaces"
1475    /// in JSON path strings. This is needed because Snowflake uses bracket notation for keys
1476    /// with special characters, but DuckDB uses quoted dot notation.
1477    fn convert_bracket_to_quoted_path(path: &str) -> String {
1478        let mut result = String::new();
1479        let mut chars = path.chars().peekable();
1480        while let Some(c) = chars.next() {
1481            if c == '[' && chars.peek() == Some(&'"') {
1482                // Found [" - start of bracket notation
1483                chars.next(); // consume "
1484                let mut key = String::new();
1485                while let Some(kc) = chars.next() {
1486                    if kc == '"' && chars.peek() == Some(&']') {
1487                        chars.next(); // consume ]
1488                        break;
1489                    }
1490                    key.push(kc);
1491                }
1492                // Convert to quoted dot notation: ."key"
1493                if !result.is_empty() && !result.ends_with('.') {
1494                    result.push('.');
1495                }
1496                result.push('"');
1497                result.push_str(&key);
1498                result.push('"');
1499            } else {
1500                result.push(c);
1501            }
1502        }
1503        result
1504    }
1505
1506    /// Transform data types according to DuckDB TYPE_MAPPING
1507    fn transform_data_type(&self, dt: crate::expressions::DataType) -> Result<Expression> {
1508        use crate::expressions::DataType;
1509        let transformed = match dt {
1510            // BINARY -> VarBinary (DuckDB generator maps VarBinary to BLOB), preserving length
1511            DataType::Binary { length } => DataType::VarBinary { length },
1512            // BLOB -> VarBinary (DuckDB generator maps VarBinary to BLOB)
1513            // This matches Python sqlglot's DuckDB parser mapping BLOB -> VARBINARY
1514            DataType::Blob => DataType::VarBinary { length: None },
1515            // CHAR/VARCHAR: Keep as-is, DuckDB generator maps to TEXT with length
1516            DataType::Char { .. } | DataType::VarChar { .. } => dt,
1517            // FLOAT -> REAL (use real_spelling flag so generator can decide)
1518            DataType::Float {
1519                precision, scale, ..
1520            } => DataType::Float {
1521                precision,
1522                scale,
1523                real_spelling: true,
1524            },
1525            // JSONB -> JSON
1526            DataType::JsonB => DataType::Json,
1527            // Handle Custom type aliases used in DuckDB
1528            DataType::Custom { ref name } => {
1529                let upper = name.to_uppercase();
1530                match upper.as_str() {
1531                    name if DataType::from_unsigned_name(name).is_some() => {
1532                        DataType::from_unsigned_name(name).unwrap()
1533                    }
1534                    // INT64 -> BIGINT
1535                    "INT64" | "INT8" => DataType::BigInt { length: None },
1536                    // INT32, INT4, SIGNED -> INT
1537                    "INT32" | "INT4" | "SIGNED" => DataType::Int {
1538                        length: None,
1539                        integer_spelling: false,
1540                    },
1541                    // INT16 -> SMALLINT
1542                    "INT16" => DataType::SmallInt { length: None },
1543                    // INT1 -> TINYINT
1544                    "INT1" => DataType::TinyInt { length: None },
1545                    // HUGEINT -> INT128
1546                    "HUGEINT" | "INT128" => DataType::Int128,
1547                    // BPCHAR -> TEXT
1548                    "BPCHAR" => DataType::Text,
1549                    // CHARACTER VARYING, CHAR VARYING -> TEXT
1550                    "CHARACTER VARYING" | "CHAR VARYING" => DataType::Text,
1551                    // FLOAT4, REAL -> REAL
1552                    "FLOAT4" => DataType::Custom {
1553                        name: "REAL".to_string(),
1554                    },
1555                    // LOGICAL -> BOOLEAN
1556                    "LOGICAL" => DataType::Boolean,
1557                    // TIMESTAMPNTZ / TIMESTAMP_NTZ -> TIMESTAMP
1558                    "TIMESTAMPNTZ" | "TIMESTAMP_NTZ" => DataType::Timestamp {
1559                        precision: None,
1560                        timezone: false,
1561                    },
1562                    // TIMESTAMP_US -> TIMESTAMP (DuckDB's default timestamp is microsecond precision)
1563                    "TIMESTAMP_US" => DataType::Timestamp {
1564                        precision: None,
1565                        timezone: false,
1566                    },
1567                    // TIMESTAMPLTZ / TIMESTAMPTZ / TIMESTAMP_LTZ / TIMESTAMP_TZ -> TIMESTAMPTZ
1568                    "TIMESTAMPLTZ" | "TIMESTAMP_LTZ" | "TIMESTAMPTZ" | "TIMESTAMP_TZ" => {
1569                        DataType::Timestamp {
1570                            precision: None,
1571                            timezone: true,
1572                        }
1573                    }
1574                    // DECFLOAT -> DECIMAL(38, 5) in DuckDB
1575                    "DECFLOAT" => DataType::Decimal {
1576                        precision: Some(38),
1577                        scale: Some(5),
1578                    },
1579                    // Keep other custom types as-is
1580                    _ => dt,
1581                }
1582            }
1583            // Keep all other types as-is
1584            other => other,
1585        };
1586        Ok(Expression::DataType(transformed))
1587    }
1588
1589    /// Transform interval to split embedded value+unit strings (e.g., '1 hour' -> '1' HOUR)
1590    /// DuckDB requires INTERVAL 'value' UNIT format, not INTERVAL 'value unit' format
1591    fn transform_interval(&self, interval: Interval) -> Result<Expression> {
1592        // Only transform if:
1593        // 1. There's a string literal value
1594        // 2. There's no unit already specified
1595        if interval.unit.is_some() {
1596            // Already has a unit, keep as-is
1597            return Ok(Expression::Interval(Box::new(interval)));
1598        }
1599
1600        if let Some(Expression::Literal(ref lit)) = interval.this {
1601            if let Literal::String(ref s) = lit.as_ref() {
1602                // Try to parse the string as "value unit" format
1603                if let Some((value, unit)) = Self::parse_interval_string(s) {
1604                    // Create new interval with separated value and unit
1605                    return Ok(Expression::Interval(Box::new(Interval {
1606                        this: Some(Expression::Literal(Box::new(Literal::String(
1607                            value.to_string(),
1608                        )))),
1609                        unit: Some(IntervalUnitSpec::Simple {
1610                            unit,
1611                            use_plural: false, // DuckDB uses singular form
1612                        }),
1613                    })));
1614                }
1615            }
1616        }
1617
1618        // No transformation needed
1619        Ok(Expression::Interval(Box::new(interval)))
1620    }
1621
1622    /// Parse an interval string like "1 hour" into (value, unit)
1623    /// Returns None if the string doesn't match the expected format
1624    fn parse_interval_string(s: &str) -> Option<(&str, IntervalUnit)> {
1625        let s = s.trim();
1626
1627        // Find where the number ends and the unit begins
1628        // Number can be: optional -, digits, optional decimal point, more digits
1629        let mut num_end = 0;
1630        let mut chars = s.chars().peekable();
1631
1632        // Skip leading minus
1633        if chars.peek() == Some(&'-') {
1634            chars.next();
1635            num_end += 1;
1636        }
1637
1638        // Skip digits
1639        while let Some(&c) = chars.peek() {
1640            if c.is_ascii_digit() {
1641                chars.next();
1642                num_end += 1;
1643            } else {
1644                break;
1645            }
1646        }
1647
1648        // Skip optional decimal point and more digits
1649        if chars.peek() == Some(&'.') {
1650            chars.next();
1651            num_end += 1;
1652            while let Some(&c) = chars.peek() {
1653                if c.is_ascii_digit() {
1654                    chars.next();
1655                    num_end += 1;
1656                } else {
1657                    break;
1658                }
1659            }
1660        }
1661
1662        if num_end == 0 || (num_end == 1 && s.starts_with('-')) {
1663            return None; // No number found
1664        }
1665
1666        let value = &s[..num_end];
1667        let rest = s[num_end..].trim();
1668
1669        // Rest should be alphabetic (the unit)
1670        if rest.is_empty() || !rest.chars().all(|c| c.is_ascii_alphabetic()) {
1671            return None;
1672        }
1673
1674        // Map unit string to IntervalUnit
1675        let unit = match rest.to_uppercase().as_str() {
1676            "YEAR" | "YEARS" | "Y" => IntervalUnit::Year,
1677            "MONTH" | "MONTHS" | "MON" | "MONS" => IntervalUnit::Month,
1678            "DAY" | "DAYS" | "D" => IntervalUnit::Day,
1679            "HOUR" | "HOURS" | "H" | "HR" | "HRS" => IntervalUnit::Hour,
1680            "MINUTE" | "MINUTES" | "MIN" | "MINS" | "M" => IntervalUnit::Minute,
1681            "SECOND" | "SECONDS" | "SEC" | "SECS" | "S" => IntervalUnit::Second,
1682            "MILLISECOND" | "MILLISECONDS" | "MS" => IntervalUnit::Millisecond,
1683            "MICROSECOND" | "MICROSECONDS" | "US" => IntervalUnit::Microsecond,
1684            "QUARTER" | "QUARTERS" | "Q" => IntervalUnit::Quarter,
1685            "WEEK" | "WEEKS" | "W" => IntervalUnit::Week,
1686            _ => return None, // Unknown unit
1687        };
1688
1689        Some((value, unit))
1690    }
1691
1692    fn transform_function(&self, f: Function) -> Result<Expression> {
1693        let name_upper = f.name.to_uppercase();
1694        match name_upper.as_str() {
1695            "FROM_HEX" if f.args.len() == 1 => {
1696                let mut args = f.args;
1697                Ok(Expression::Unhex(Box::new(Unhex {
1698                    this: Box::new(args.remove(0)),
1699                    expression: None,
1700                })))
1701            }
1702
1703            // IFNULL -> COALESCE
1704            "IFNULL" if f.args.len() == 2 => Ok(Expression::Coalesce(Box::new(VarArgFunc {
1705                original_name: None,
1706                expressions: f.args,
1707                inferred_type: None,
1708            }))),
1709
1710            // NVL -> COALESCE
1711            "NVL" if f.args.len() == 2 => Ok(Expression::Coalesce(Box::new(VarArgFunc {
1712                original_name: None,
1713                expressions: f.args,
1714                inferred_type: None,
1715            }))),
1716
1717            // ISNULL -> COALESCE
1718            "ISNULL" if f.args.len() == 2 => Ok(Expression::Coalesce(Box::new(VarArgFunc {
1719                original_name: None,
1720                expressions: f.args,
1721                inferred_type: None,
1722            }))),
1723
1724            // ARRAY_COMPACT(arr) -> LIST_FILTER(arr, _u -> NOT _u IS NULL)
1725            "ARRAY_COMPACT" if f.args.len() == 1 => {
1726                let arr = f.args.into_iter().next().unwrap();
1727                let lambda = Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
1728                    parameters: vec![Identifier::new("_u".to_string())],
1729                    body: Expression::Not(Box::new(crate::expressions::UnaryOp {
1730                        this: Expression::IsNull(Box::new(crate::expressions::IsNull {
1731                            this: Expression::boxed_column(Column {
1732                                table: None,
1733                                name: Identifier::new("_u".to_string()),
1734                                join_mark: false,
1735                                trailing_comments: Vec::new(),
1736                                span: None,
1737                                inferred_type: None,
1738                            }),
1739                            not: false,
1740                            postfix_form: false,
1741                        })),
1742                        inferred_type: None,
1743                    })),
1744                    colon: false,
1745                    parameter_types: Vec::new(),
1746                }));
1747                Ok(Expression::Function(Box::new(Function::new(
1748                    "LIST_FILTER".to_string(),
1749                    vec![arr, lambda],
1750                ))))
1751            }
1752
1753            // ARRAY_CONSTRUCT_COMPACT: handled in the generator (to avoid source-transform interference)
1754            "ARRAY_CONSTRUCT_COMPACT" => Ok(Expression::Function(Box::new(f))),
1755
1756            // GROUP_CONCAT -> LISTAGG in DuckDB
1757            "GROUP_CONCAT" if !f.args.is_empty() => Ok(Expression::Function(Box::new(
1758                Function::new("LISTAGG".to_string(), f.args),
1759            ))),
1760
1761            // LISTAGG is native to DuckDB
1762            "LISTAGG" => Ok(Expression::Function(Box::new(f))),
1763
1764            // STRING_AGG -> LISTAGG in DuckDB
1765            "STRING_AGG" if !f.args.is_empty() => Ok(Expression::Function(Box::new(
1766                Function::new("LISTAGG".to_string(), f.args),
1767            ))),
1768
1769            // ARRAY_UNIQUE_AGG(col) -> LIST(DISTINCT col) FILTER(WHERE NOT col IS NULL)
1770            "ARRAY_UNIQUE_AGG" if f.args.len() == 1 => {
1771                let col = f.args.into_iter().next().unwrap();
1772                // NOT col IS NULL
1773                let filter_expr = Expression::Not(Box::new(UnaryOp {
1774                    this: Expression::IsNull(Box::new(IsNull {
1775                        this: col.clone(),
1776                        not: false,
1777                        postfix_form: false,
1778                    })),
1779                    inferred_type: None,
1780                }));
1781                Ok(Expression::ArrayAgg(Box::new(AggFunc {
1782                    this: col,
1783                    distinct: true,
1784                    filter: Some(filter_expr),
1785                    order_by: Vec::new(),
1786                    name: Some("LIST".to_string()),
1787                    ignore_nulls: None,
1788                    having_max: None,
1789                    limit: None,
1790                    inferred_type: None,
1791                })))
1792            }
1793
1794            // CHECK_JSON(x) -> CASE WHEN x IS NULL OR x = '' OR JSON_VALID(x) THEN NULL ELSE 'Invalid JSON' END
1795            "CHECK_JSON" if f.args.len() == 1 => {
1796                let x = f.args.into_iter().next().unwrap();
1797                // x IS NULL
1798                let is_null = Expression::IsNull(Box::new(IsNull {
1799                    this: x.clone(),
1800                    not: false,
1801                    postfix_form: false,
1802                }));
1803                // x = ''
1804                let eq_empty = Expression::Eq(Box::new(BinaryOp::new(
1805                    x.clone(),
1806                    Expression::Literal(Box::new(Literal::String(String::new()))),
1807                )));
1808                // JSON_VALID(x)
1809                let json_valid = Expression::Function(Box::new(Function::new(
1810                    "JSON_VALID".to_string(),
1811                    vec![x],
1812                )));
1813                // x IS NULL OR x = ''
1814                let or1 = Expression::Or(Box::new(BinaryOp::new(is_null, eq_empty)));
1815                // (x IS NULL OR x = '') OR JSON_VALID(x)
1816                let condition = Expression::Or(Box::new(BinaryOp::new(or1, json_valid)));
1817                Ok(Expression::Case(Box::new(Case {
1818                    operand: None,
1819                    whens: vec![(condition, Expression::Null(Null))],
1820                    else_: Some(Expression::Literal(Box::new(Literal::String(
1821                        "Invalid JSON".to_string(),
1822                    )))),
1823                    comments: Vec::new(),
1824                    inferred_type: None,
1825                })))
1826            }
1827
1828            // SUBSTR is native in DuckDB (keep as-is, don't convert to SUBSTRING)
1829            "SUBSTR" => Ok(Expression::Function(Box::new(f))),
1830
1831            // FLATTEN -> UNNEST in DuckDB
1832            "FLATTEN" => Ok(Expression::Function(Box::new(Function::new(
1833                "UNNEST".to_string(),
1834                f.args,
1835            )))),
1836
1837            // ARRAY_FLATTEN -> FLATTEN in DuckDB
1838            "ARRAY_FLATTEN" => Ok(Expression::Function(Box::new(Function::new(
1839                "FLATTEN".to_string(),
1840                f.args,
1841            )))),
1842
1843            // RPAD with 2 args -> RPAD with 3 args (default padding ' ')
1844            "RPAD" if f.args.len() == 2 => {
1845                let mut args = f.args;
1846                args.push(Expression::Literal(Box::new(Literal::String(
1847                    " ".to_string(),
1848                ))));
1849                Ok(Expression::Function(Box::new(Function::new(
1850                    "RPAD".to_string(),
1851                    args,
1852                ))))
1853            }
1854
1855            // BASE64_DECODE_STRING(x) -> DECODE(FROM_BASE64(x))
1856            // BASE64_DECODE_STRING(x, alphabet) -> DECODE(FROM_BASE64(REPLACE(REPLACE(REPLACE(x, '-', '+'), '_', '/'), '+', '=')))
1857            "BASE64_DECODE_STRING" => {
1858                let mut args = f.args;
1859                let input = args.remove(0);
1860                let has_alphabet = !args.is_empty();
1861                let decoded_input = if has_alphabet {
1862                    // Apply alphabet replacements: REPLACE(REPLACE(REPLACE(x, '-', '+'), '_', '/'), '+', '=')
1863                    let r1 = Expression::Function(Box::new(Function::new(
1864                        "REPLACE".to_string(),
1865                        vec![
1866                            input,
1867                            Expression::Literal(Box::new(Literal::String("-".to_string()))),
1868                            Expression::Literal(Box::new(Literal::String("+".to_string()))),
1869                        ],
1870                    )));
1871                    let r2 = Expression::Function(Box::new(Function::new(
1872                        "REPLACE".to_string(),
1873                        vec![
1874                            r1,
1875                            Expression::Literal(Box::new(Literal::String("_".to_string()))),
1876                            Expression::Literal(Box::new(Literal::String("/".to_string()))),
1877                        ],
1878                    )));
1879                    Expression::Function(Box::new(Function::new(
1880                        "REPLACE".to_string(),
1881                        vec![
1882                            r2,
1883                            Expression::Literal(Box::new(Literal::String("+".to_string()))),
1884                            Expression::Literal(Box::new(Literal::String("=".to_string()))),
1885                        ],
1886                    )))
1887                } else {
1888                    input
1889                };
1890                let from_base64 = Expression::Function(Box::new(Function::new(
1891                    "FROM_BASE64".to_string(),
1892                    vec![decoded_input],
1893                )));
1894                Ok(Expression::Function(Box::new(Function::new(
1895                    "DECODE".to_string(),
1896                    vec![from_base64],
1897                ))))
1898            }
1899
1900            // BASE64_DECODE_BINARY(x) -> FROM_BASE64(x)
1901            // BASE64_DECODE_BINARY(x, alphabet) -> FROM_BASE64(REPLACE(REPLACE(REPLACE(x, '-', '+'), '_', '/'), '+', '='))
1902            "BASE64_DECODE_BINARY" => {
1903                let mut args = f.args;
1904                let input = args.remove(0);
1905                let has_alphabet = !args.is_empty();
1906                let decoded_input = if has_alphabet {
1907                    let r1 = Expression::Function(Box::new(Function::new(
1908                        "REPLACE".to_string(),
1909                        vec![
1910                            input,
1911                            Expression::Literal(Box::new(Literal::String("-".to_string()))),
1912                            Expression::Literal(Box::new(Literal::String("+".to_string()))),
1913                        ],
1914                    )));
1915                    let r2 = Expression::Function(Box::new(Function::new(
1916                        "REPLACE".to_string(),
1917                        vec![
1918                            r1,
1919                            Expression::Literal(Box::new(Literal::String("_".to_string()))),
1920                            Expression::Literal(Box::new(Literal::String("/".to_string()))),
1921                        ],
1922                    )));
1923                    Expression::Function(Box::new(Function::new(
1924                        "REPLACE".to_string(),
1925                        vec![
1926                            r2,
1927                            Expression::Literal(Box::new(Literal::String("+".to_string()))),
1928                            Expression::Literal(Box::new(Literal::String("=".to_string()))),
1929                        ],
1930                    )))
1931                } else {
1932                    input
1933                };
1934                Ok(Expression::Function(Box::new(Function::new(
1935                    "FROM_BASE64".to_string(),
1936                    vec![decoded_input],
1937                ))))
1938            }
1939
1940            // SPACE(n) -> REPEAT(' ', CAST(n AS BIGINT))
1941            "SPACE" if f.args.len() == 1 => {
1942                let arg = f.args.into_iter().next().unwrap();
1943                let cast_arg = Expression::Cast(Box::new(Cast {
1944                    this: arg,
1945                    to: DataType::BigInt { length: None },
1946                    trailing_comments: Vec::new(),
1947                    double_colon_syntax: false,
1948                    format: None,
1949                    default: None,
1950                    inferred_type: None,
1951                }));
1952                Ok(Expression::Function(Box::new(Function::new(
1953                    "REPEAT".to_string(),
1954                    vec![
1955                        Expression::Literal(Box::new(Literal::String(" ".to_string()))),
1956                        cast_arg,
1957                    ],
1958                ))))
1959            }
1960
1961            // IS_ARRAY(x) -> JSON_TYPE(x) = 'ARRAY'
1962            "IS_ARRAY" if f.args.len() == 1 => {
1963                let arg = f.args.into_iter().next().unwrap();
1964                let json_type = Expression::Function(Box::new(Function::new(
1965                    "JSON_TYPE".to_string(),
1966                    vec![arg],
1967                )));
1968                Ok(Expression::Eq(Box::new(BinaryOp {
1969                    left: json_type,
1970                    right: Expression::Literal(Box::new(Literal::String("ARRAY".to_string()))),
1971                    left_comments: Vec::new(),
1972                    operator_comments: Vec::new(),
1973                    trailing_comments: Vec::new(),
1974                    inferred_type: None,
1975                })))
1976            }
1977
1978            // EXPLODE -> UNNEST
1979            "EXPLODE" => Ok(Expression::Function(Box::new(Function::new(
1980                "UNNEST".to_string(),
1981                f.args,
1982            )))),
1983
1984            // GETDATE -> CURRENT_TIMESTAMP
1985            "GETDATE" => Ok(Expression::CurrentTimestamp(
1986                crate::expressions::CurrentTimestamp {
1987                    precision: None,
1988                    sysdate: false,
1989                },
1990            )),
1991
1992            // TODAY -> CURRENT_DATE
1993            "TODAY" => Ok(Expression::CurrentDate(crate::expressions::CurrentDate)),
1994
1995            // CURDATE -> CURRENT_DATE
1996            "CURDATE" => Ok(Expression::CurrentDate(crate::expressions::CurrentDate)),
1997
1998            // GET_CURRENT_TIME -> CURRENT_TIME
1999            "GET_CURRENT_TIME" => Ok(Expression::CurrentTime(crate::expressions::CurrentTime {
2000                precision: None,
2001            })),
2002
2003            // CURRENT_LOCALTIMESTAMP -> LOCALTIMESTAMP
2004            "CURRENT_LOCALTIMESTAMP" => Ok(Expression::Localtimestamp(Box::new(
2005                crate::expressions::Localtimestamp { this: None },
2006            ))),
2007
2008            // REGEXP_EXTRACT_ALL: strip default group_idx=0
2009            "REGEXP_EXTRACT_ALL" if f.args.len() == 3 => {
2010                // If third arg is literal 0, strip it
2011                if matches!(&f.args[2], Expression::Literal(lit) if matches!(lit.as_ref(), crate::expressions::Literal::Number(n) if n == "0"))
2012                {
2013                    Ok(Expression::Function(Box::new(Function::new(
2014                        "REGEXP_EXTRACT_ALL".to_string(),
2015                        vec![f.args[0].clone(), f.args[1].clone()],
2016                    ))))
2017                } else {
2018                    Ok(Expression::Function(Box::new(Function::new(
2019                        "REGEXP_EXTRACT_ALL".to_string(),
2020                        f.args,
2021                    ))))
2022                }
2023            }
2024
2025            // CURRENT_DATE is native
2026            "CURRENT_DATE" => Ok(Expression::CurrentDate(crate::expressions::CurrentDate)),
2027
2028            // TO_DATE with 1 arg -> CAST(x AS DATE)
2029            "TO_DATE" if f.args.len() == 1 => {
2030                let arg = f.args.into_iter().next().unwrap();
2031                Ok(Expression::Cast(Box::new(Cast {
2032                    this: arg,
2033                    to: DataType::Date,
2034                    trailing_comments: Vec::new(),
2035                    double_colon_syntax: false,
2036                    format: None,
2037                    default: None,
2038                    inferred_type: None,
2039                })))
2040            }
2041
2042            // TO_TIMESTAMP is native in DuckDB (kept as-is for identity)
2043
2044            // DATE_FORMAT -> STRFTIME in DuckDB with format conversion
2045            "DATE_FORMAT" if f.args.len() >= 2 => {
2046                let mut args = f.args;
2047                args[1] = Self::convert_format_to_duckdb(&args[1]);
2048                Ok(Expression::Function(Box::new(Function::new(
2049                    "STRFTIME".to_string(),
2050                    args,
2051                ))))
2052            }
2053
2054            // DATE_PARSE -> STRPTIME in DuckDB with format conversion
2055            "DATE_PARSE" if f.args.len() >= 2 => {
2056                let mut args = f.args;
2057                args[1] = Self::convert_format_to_duckdb(&args[1]);
2058                Ok(Expression::Function(Box::new(Function::new(
2059                    "STRPTIME".to_string(),
2060                    args,
2061                ))))
2062            }
2063
2064            // FORMAT_DATE -> STRFTIME in DuckDB
2065            "FORMAT_DATE" if f.args.len() >= 2 => {
2066                let mut args = f.args;
2067                args[1] = Self::convert_format_to_duckdb(&args[1]);
2068                Ok(Expression::Function(Box::new(Function::new(
2069                    "STRFTIME".to_string(),
2070                    args,
2071                ))))
2072            }
2073
2074            // TO_CHAR -> STRFTIME in DuckDB
2075            "TO_CHAR" if f.args.len() >= 2 => {
2076                let mut args = f.args;
2077                args[1] = Self::convert_format_to_duckdb(&args[1]);
2078                Ok(Expression::Function(Box::new(Function::new(
2079                    "STRFTIME".to_string(),
2080                    args,
2081                ))))
2082            }
2083
2084            // EPOCH_MS is native to DuckDB
2085            "EPOCH_MS" => Ok(Expression::Function(Box::new(f))),
2086
2087            // EPOCH -> EPOCH (native)
2088            "EPOCH" => Ok(Expression::Function(Box::new(f))),
2089
2090            // FROM_UNIXTIME -> TO_TIMESTAMP in DuckDB
2091            "FROM_UNIXTIME" if !f.args.is_empty() => Ok(Expression::Function(Box::new(
2092                Function::new("TO_TIMESTAMP".to_string(), f.args),
2093            ))),
2094
2095            // UNIX_TIMESTAMP -> EPOCH
2096            "UNIX_TIMESTAMP" => Ok(Expression::Function(Box::new(Function::new(
2097                "EPOCH".to_string(),
2098                f.args,
2099            )))),
2100
2101            // JSON_EXTRACT -> arrow operator (->)
2102            "JSON_EXTRACT" if f.args.len() == 2 => {
2103                let mut args = f.args;
2104                let path = args.pop().unwrap();
2105                let this = args.pop().unwrap();
2106                Ok(Expression::JsonExtract(Box::new(JsonExtractFunc {
2107                    options: None,
2108                    source_dialect: None,
2109
2110                    this,
2111                    path,
2112                    returning: None,
2113                    arrow_syntax: true,
2114                    hash_arrow_syntax: false,
2115                    wrapper_option: None,
2116                    quotes_option: None,
2117                    on_scalar_string: false,
2118                    on_error: None,
2119                })))
2120            }
2121
2122            // JSON_EXTRACT_STRING -> arrow operator (->>)
2123            "JSON_EXTRACT_STRING" if f.args.len() == 2 => {
2124                let mut args = f.args;
2125                let path = args.pop().unwrap();
2126                let this = args.pop().unwrap();
2127                Ok(Expression::JsonExtractScalar(Box::new(JsonExtractFunc {
2128                    options: None,
2129                    source_dialect: None,
2130
2131                    this,
2132                    path,
2133                    returning: None,
2134                    arrow_syntax: true,
2135                    hash_arrow_syntax: false,
2136                    wrapper_option: None,
2137                    quotes_option: None,
2138                    on_scalar_string: false,
2139                    on_error: None,
2140                })))
2141            }
2142
2143            // ARRAY_CONSTRUCT -> list_value or [a, b, c] syntax
2144            "ARRAY_CONSTRUCT" => Ok(Expression::Function(Box::new(Function::new(
2145                "list_value".to_string(),
2146                f.args,
2147            )))),
2148
2149            // ARRAY -> list_value
2150            // ARRAY -> list_value for non-subquery args, keep ARRAY for subquery args
2151            "ARRAY" => {
2152                // Check if any arg contains a query (subquery)
2153                let has_query = f
2154                    .args
2155                    .iter()
2156                    .any(|a| matches!(a, Expression::Subquery(_) | Expression::Select(_)));
2157                if has_query {
2158                    // Keep as ARRAY() for subquery args
2159                    Ok(Expression::Function(Box::new(Function::new(
2160                        "ARRAY".to_string(),
2161                        f.args,
2162                    ))))
2163                } else {
2164                    Ok(Expression::Function(Box::new(Function::new(
2165                        "list_value".to_string(),
2166                        f.args,
2167                    ))))
2168                }
2169            }
2170
2171            // LIST_VALUE -> Array literal notation [...]
2172            "LIST_VALUE" => Ok(Expression::Array(Box::new(crate::expressions::Array {
2173                expressions: f.args,
2174                inferred_type: None,
2175            }))),
2176
2177            // ARRAY_AGG -> LIST in DuckDB (or array_agg which is also supported)
2178            "ARRAY_AGG" => Ok(Expression::Function(Box::new(Function::new(
2179                "list".to_string(),
2180                f.args,
2181            )))),
2182
2183            // LIST_CONTAINS / ARRAY_CONTAINS -> keep normalized form
2184            "LIST_CONTAINS" | "ARRAY_CONTAINS" => Ok(Expression::Function(Box::new(
2185                Function::new("ARRAY_CONTAINS".to_string(), f.args),
2186            ))),
2187
2188            // ARRAY_SIZE/CARDINALITY -> ARRAY_LENGTH in DuckDB
2189            "ARRAY_SIZE" | "CARDINALITY" => Ok(Expression::Function(Box::new(Function::new(
2190                "ARRAY_LENGTH".to_string(),
2191                f.args,
2192            )))),
2193
2194            // LEN -> LENGTH
2195            "LEN" if f.args.len() == 1 => Ok(Expression::Length(Box::new(UnaryFunc::new(
2196                f.args.into_iter().next().unwrap(),
2197            )))),
2198
2199            // CEILING -> CEIL (both work)
2200            "CEILING" if f.args.len() == 1 => Ok(Expression::Ceil(Box::new(CeilFunc {
2201                this: f.args.into_iter().next().unwrap(),
2202                decimals: None,
2203                to: None,
2204            }))),
2205
2206            // LOGICAL_OR -> BOOL_OR with CAST to BOOLEAN
2207            "LOGICAL_OR" if f.args.len() == 1 => {
2208                let arg = f.args.into_iter().next().unwrap();
2209                Ok(Expression::Function(Box::new(Function::new(
2210                    "BOOL_OR".to_string(),
2211                    vec![Expression::Cast(Box::new(crate::expressions::Cast {
2212                        this: arg,
2213                        to: crate::expressions::DataType::Boolean,
2214                        trailing_comments: Vec::new(),
2215                        double_colon_syntax: false,
2216                        format: None,
2217                        default: None,
2218                        inferred_type: None,
2219                    }))],
2220                ))))
2221            }
2222
2223            // LOGICAL_AND -> BOOL_AND with CAST to BOOLEAN
2224            "LOGICAL_AND" if f.args.len() == 1 => {
2225                let arg = f.args.into_iter().next().unwrap();
2226                Ok(Expression::Function(Box::new(Function::new(
2227                    "BOOL_AND".to_string(),
2228                    vec![Expression::Cast(Box::new(crate::expressions::Cast {
2229                        this: arg,
2230                        to: crate::expressions::DataType::Boolean,
2231                        trailing_comments: Vec::new(),
2232                        double_colon_syntax: false,
2233                        format: None,
2234                        default: None,
2235                        inferred_type: None,
2236                    }))],
2237                ))))
2238            }
2239
2240            // REGEXP_LIKE -> REGEXP_MATCHES in DuckDB
2241            "REGEXP_LIKE" => Ok(Expression::Function(Box::new(Function::new(
2242                "REGEXP_MATCHES".to_string(),
2243                f.args,
2244            )))),
2245
2246            // POSITION is native
2247            "POSITION" => Ok(Expression::Function(Box::new(f))),
2248
2249            // CHARINDEX(substr, str) -> STRPOS(str, substr) in DuckDB
2250            "CHARINDEX" if f.args.len() == 2 => {
2251                let mut args = f.args;
2252                let substr = args.remove(0);
2253                let str_expr = args.remove(0);
2254                Ok(Expression::Function(Box::new(Function::new(
2255                    "STRPOS".to_string(),
2256                    vec![str_expr, substr],
2257                ))))
2258            }
2259
2260            // CHARINDEX(substr, str, pos) -> complex CASE expression for DuckDB
2261            // CASE WHEN STRPOS(SUBSTRING(str, CASE WHEN pos <= 0 THEN 1 ELSE pos END), substr) = 0
2262            // THEN 0
2263            // ELSE STRPOS(SUBSTRING(str, CASE WHEN pos <= 0 THEN 1 ELSE pos END), substr) + CASE WHEN pos <= 0 THEN 1 ELSE pos END - 1
2264            // END
2265            "CHARINDEX" if f.args.len() == 3 => {
2266                let mut args = f.args;
2267                let substr = args.remove(0);
2268                let str_expr = args.remove(0);
2269                let pos = args.remove(0);
2270
2271                let zero = Expression::Literal(Box::new(Literal::Number("0".to_string())));
2272                let one = Expression::Literal(Box::new(Literal::Number("1".to_string())));
2273
2274                // CASE WHEN pos <= 0 THEN 1 ELSE pos END
2275                let pos_case = Expression::Case(Box::new(Case {
2276                    operand: None,
2277                    whens: vec![(
2278                        Expression::Lte(Box::new(BinaryOp::new(pos.clone(), zero.clone()))),
2279                        one.clone(),
2280                    )],
2281                    else_: Some(pos.clone()),
2282                    comments: Vec::new(),
2283                    inferred_type: None,
2284                }));
2285
2286                // SUBSTRING(str, pos_case)
2287                let substring_expr = Expression::Substring(Box::new(SubstringFunc {
2288                    this: str_expr,
2289                    start: pos_case.clone(),
2290                    length: None,
2291                    from_for_syntax: false,
2292                }));
2293
2294                // STRPOS(SUBSTRING(...), substr)
2295                let strpos = Expression::Function(Box::new(Function::new(
2296                    "STRPOS".to_string(),
2297                    vec![substring_expr, substr],
2298                )));
2299
2300                // STRPOS(...) = 0
2301                let eq_zero = Expression::Eq(Box::new(BinaryOp::new(strpos.clone(), zero.clone())));
2302
2303                // STRPOS(...) + pos_case - 1
2304                let add_pos = Expression::Add(Box::new(BinaryOp::new(strpos, pos_case)));
2305                let sub_one = Expression::Sub(Box::new(BinaryOp::new(add_pos, one)));
2306
2307                Ok(Expression::Case(Box::new(Case {
2308                    operand: None,
2309                    whens: vec![(eq_zero, zero)],
2310                    else_: Some(sub_one),
2311                    comments: Vec::new(),
2312                    inferred_type: None,
2313                })))
2314            }
2315
2316            // SPLIT -> STR_SPLIT in DuckDB
2317            "SPLIT" => Ok(Expression::Function(Box::new(Function::new(
2318                "STR_SPLIT".to_string(),
2319                f.args,
2320            )))),
2321
2322            // STRING_SPLIT -> STR_SPLIT in DuckDB
2323            "STRING_SPLIT" => Ok(Expression::Function(Box::new(Function::new(
2324                "STR_SPLIT".to_string(),
2325                f.args,
2326            )))),
2327
2328            // STRTOK_TO_ARRAY -> STR_SPLIT
2329            "STRTOK_TO_ARRAY" => Ok(Expression::Function(Box::new(Function::new(
2330                "STR_SPLIT".to_string(),
2331                f.args,
2332            )))),
2333
2334            // REGEXP_SPLIT -> STR_SPLIT_REGEX in DuckDB
2335            "REGEXP_SPLIT" => Ok(Expression::Function(Box::new(Function::new(
2336                "STR_SPLIT_REGEX".to_string(),
2337                f.args,
2338            )))),
2339
2340            // EDITDIST3 -> LEVENSHTEIN in DuckDB
2341            "EDITDIST3" => Ok(Expression::Function(Box::new(Function::new(
2342                "LEVENSHTEIN".to_string(),
2343                f.args,
2344            )))),
2345
2346            // JSON_EXTRACT_PATH -> arrow operator (->)
2347            "JSON_EXTRACT_PATH" if f.args.len() >= 2 => {
2348                let mut args = f.args;
2349                let this = args.remove(0);
2350                let path = args.remove(0);
2351                Ok(Expression::JsonExtract(Box::new(JsonExtractFunc {
2352                    options: None,
2353                    source_dialect: None,
2354
2355                    this,
2356                    path,
2357                    returning: None,
2358                    arrow_syntax: true,
2359                    hash_arrow_syntax: false,
2360                    wrapper_option: None,
2361                    quotes_option: None,
2362                    on_scalar_string: false,
2363                    on_error: None,
2364                })))
2365            }
2366
2367            // JSON_EXTRACT_PATH_TEXT -> arrow operator (->>)
2368            "JSON_EXTRACT_PATH_TEXT" if f.args.len() >= 2 => {
2369                let mut args = f.args;
2370                let this = args.remove(0);
2371                let path = args.remove(0);
2372                Ok(Expression::JsonExtractScalar(Box::new(JsonExtractFunc {
2373                    options: None,
2374                    source_dialect: None,
2375
2376                    this,
2377                    path,
2378                    returning: None,
2379                    arrow_syntax: true,
2380                    hash_arrow_syntax: false,
2381                    wrapper_option: None,
2382                    quotes_option: None,
2383                    on_scalar_string: false,
2384                    on_error: None,
2385                })))
2386            }
2387
2388            // DATE_ADD(date, interval) -> date + interval in DuckDB
2389            "DATE_ADD" if f.args.len() == 2 => {
2390                let mut args = f.args;
2391                let date = args.remove(0);
2392                let interval = args.remove(0);
2393                Ok(Expression::Add(Box::new(BinaryOp {
2394                    left: date,
2395                    right: interval,
2396                    left_comments: Vec::new(),
2397                    operator_comments: Vec::new(),
2398                    trailing_comments: Vec::new(),
2399                    inferred_type: None,
2400                })))
2401            }
2402
2403            // DATE_SUB(date, interval) -> date - interval in DuckDB
2404            "DATE_SUB" if f.args.len() == 2 => {
2405                let mut args = f.args;
2406                let date = args.remove(0);
2407                let interval = args.remove(0);
2408                Ok(Expression::Sub(Box::new(BinaryOp {
2409                    left: date,
2410                    right: interval,
2411                    left_comments: Vec::new(),
2412                    operator_comments: Vec::new(),
2413                    trailing_comments: Vec::new(),
2414                    inferred_type: None,
2415                })))
2416            }
2417
2418            // RANGE(n) -> RANGE(0, n) in DuckDB
2419            "RANGE" if f.args.len() == 1 => {
2420                let mut new_args = vec![Expression::number(0)];
2421                new_args.extend(f.args);
2422                Ok(Expression::Function(Box::new(Function::new(
2423                    "RANGE".to_string(),
2424                    new_args,
2425                ))))
2426            }
2427
2428            // GENERATE_SERIES(n) -> GENERATE_SERIES(0, n) in DuckDB
2429            "GENERATE_SERIES" if f.args.len() == 1 => {
2430                let mut new_args = vec![Expression::number(0)];
2431                new_args.extend(f.args);
2432                Ok(Expression::Function(Box::new(Function::new(
2433                    "GENERATE_SERIES".to_string(),
2434                    new_args,
2435                ))))
2436            }
2437
2438            // REGEXP_EXTRACT(str, pattern, 0) -> REGEXP_EXTRACT(str, pattern) in DuckDB
2439            // Drop the group argument when it's 0 (default)
2440            "REGEXP_EXTRACT" if f.args.len() == 3 => {
2441                // Check if the third argument is 0
2442                let drop_group = match &f.args[2] {
2443                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => {
2444                        let Literal::Number(n) = lit.as_ref() else {
2445                            unreachable!()
2446                        };
2447                        n == "0"
2448                    }
2449                    _ => false,
2450                };
2451                if drop_group {
2452                    Ok(Expression::Function(Box::new(Function::new(
2453                        "REGEXP_EXTRACT".to_string(),
2454                        vec![f.args[0].clone(), f.args[1].clone()],
2455                    ))))
2456                } else {
2457                    Ok(Expression::Function(Box::new(f)))
2458                }
2459            }
2460
2461            // STRUCT_PACK(a := 1, b := 2) -> {'a': 1, 'b': 2} (DuckDB struct literal)
2462            "STRUCT_PACK" => {
2463                let mut fields = Vec::new();
2464                for arg in f.args {
2465                    match arg {
2466                        Expression::NamedArgument(na) => {
2467                            fields.push((Some(na.name.name.clone()), na.value));
2468                        }
2469                        // Non-named arguments get positional keys
2470                        other => {
2471                            fields.push((None, other));
2472                        }
2473                    }
2474                }
2475                Ok(Expression::Struct(Box::new(Struct { fields })))
2476            }
2477
2478            // REPLACE with 2 args -> add empty string 3rd arg
2479            "REPLACE" if f.args.len() == 2 => {
2480                let mut args = f.args;
2481                args.push(Expression::Literal(Box::new(
2482                    Literal::String(String::new()),
2483                )));
2484                Ok(Expression::Function(Box::new(Function::new(
2485                    "REPLACE".to_string(),
2486                    args,
2487                ))))
2488            }
2489
2490            // TO_UNIXTIME -> EPOCH in DuckDB
2491            "TO_UNIXTIME" => Ok(Expression::Function(Box::new(Function::new(
2492                "EPOCH".to_string(),
2493                f.args,
2494            )))),
2495
2496            // FROM_ISO8601_TIMESTAMP -> CAST(x AS TIMESTAMPTZ) in DuckDB
2497            "FROM_ISO8601_TIMESTAMP" if f.args.len() == 1 => {
2498                use crate::expressions::{Cast, DataType};
2499                Ok(Expression::Cast(Box::new(Cast {
2500                    this: f.args.into_iter().next().unwrap(),
2501                    to: DataType::Timestamp {
2502                        precision: None,
2503                        timezone: true,
2504                    },
2505                    trailing_comments: Vec::new(),
2506                    double_colon_syntax: false,
2507                    format: None,
2508                    default: None,
2509                    inferred_type: None,
2510                })))
2511            }
2512
2513            // APPROX_DISTINCT -> APPROX_COUNT_DISTINCT in DuckDB
2514            "APPROX_DISTINCT" => {
2515                // Drop the accuracy parameter (second arg) if present
2516                let args = if f.args.len() > 1 {
2517                    vec![f.args.into_iter().next().unwrap()]
2518                } else {
2519                    f.args
2520                };
2521                Ok(Expression::Function(Box::new(Function::new(
2522                    "APPROX_COUNT_DISTINCT".to_string(),
2523                    args,
2524                ))))
2525            }
2526
2527            // ARRAY_SORT is native to DuckDB (but drop the lambda comparator)
2528            "ARRAY_SORT" => {
2529                let args = vec![f.args.into_iter().next().unwrap()];
2530                Ok(Expression::Function(Box::new(Function::new(
2531                    "ARRAY_SORT".to_string(),
2532                    args,
2533                ))))
2534            }
2535
2536            // TO_UTF8 -> ENCODE in DuckDB
2537            "TO_UTF8" => Ok(Expression::Function(Box::new(Function::new(
2538                "ENCODE".to_string(),
2539                f.args,
2540            )))),
2541
2542            // FROM_UTF8 -> DECODE in DuckDB
2543            "FROM_UTF8" => Ok(Expression::Function(Box::new(Function::new(
2544                "DECODE".to_string(),
2545                f.args,
2546            )))),
2547
2548            // ARBITRARY -> ANY_VALUE in DuckDB
2549            "ARBITRARY" => Ok(Expression::Function(Box::new(Function::new(
2550                "ANY_VALUE".to_string(),
2551                f.args,
2552            )))),
2553
2554            // MAX_BY -> ARG_MAX in DuckDB
2555            "MAX_BY" => Ok(Expression::Function(Box::new(Function::new(
2556                "ARG_MAX".to_string(),
2557                f.args,
2558            )))),
2559
2560            // MIN_BY -> ARG_MIN in DuckDB
2561            "MIN_BY" => Ok(Expression::Function(Box::new(Function::new(
2562                "ARG_MIN".to_string(),
2563                f.args,
2564            )))),
2565
2566            // ===== Snowflake-specific function transforms =====
2567            "IFF" if f.args.len() == 3 => {
2568                let mut args = f.args;
2569                let cond = args.remove(0);
2570                let true_val = args.remove(0);
2571                let false_val = args.remove(0);
2572                Ok(Expression::Case(Box::new(Case {
2573                    operand: None,
2574                    whens: vec![(cond, true_val)],
2575                    else_: Some(false_val),
2576                    comments: Vec::new(),
2577                    inferred_type: None,
2578                })))
2579            }
2580            "SKEW" => Ok(Expression::Function(Box::new(Function::new(
2581                "SKEWNESS".to_string(),
2582                f.args,
2583            )))),
2584            "VAR_SAMP" => Ok(Expression::Function(Box::new(Function::new(
2585                "VARIANCE".to_string(),
2586                f.args,
2587            )))),
2588            "VARIANCE_POP" => Ok(Expression::Function(Box::new(Function::new(
2589                "VAR_POP".to_string(),
2590                f.args,
2591            )))),
2592            "REGR_VALX" if f.args.len() == 2 => {
2593                let mut args = f.args;
2594                let y = args.remove(0);
2595                let x = args.remove(0);
2596                Ok(Expression::Case(Box::new(Case {
2597                    operand: None,
2598                    whens: vec![(
2599                        Expression::IsNull(Box::new(crate::expressions::IsNull {
2600                            this: y,
2601                            not: false,
2602                            postfix_form: false,
2603                        })),
2604                        Expression::Cast(Box::new(Cast {
2605                            this: Expression::Null(crate::expressions::Null),
2606                            to: DataType::Double {
2607                                precision: None,
2608                                scale: None,
2609                            },
2610                            trailing_comments: Vec::new(),
2611                            double_colon_syntax: false,
2612                            format: None,
2613                            default: None,
2614                            inferred_type: None,
2615                        })),
2616                    )],
2617                    else_: Some(x),
2618                    comments: Vec::new(),
2619                    inferred_type: None,
2620                })))
2621            }
2622            "REGR_VALY" if f.args.len() == 2 => {
2623                let mut args = f.args;
2624                let y = args.remove(0);
2625                let x = args.remove(0);
2626                Ok(Expression::Case(Box::new(Case {
2627                    operand: None,
2628                    whens: vec![(
2629                        Expression::IsNull(Box::new(crate::expressions::IsNull {
2630                            this: x,
2631                            not: false,
2632                            postfix_form: false,
2633                        })),
2634                        Expression::Cast(Box::new(Cast {
2635                            this: Expression::Null(crate::expressions::Null),
2636                            to: DataType::Double {
2637                                precision: None,
2638                                scale: None,
2639                            },
2640                            trailing_comments: Vec::new(),
2641                            double_colon_syntax: false,
2642                            format: None,
2643                            default: None,
2644                            inferred_type: None,
2645                        })),
2646                    )],
2647                    else_: Some(y),
2648                    comments: Vec::new(),
2649                    inferred_type: None,
2650                })))
2651            }
2652            "BOOLNOT" if f.args.len() == 1 => {
2653                let arg = f.args.into_iter().next().unwrap();
2654                // BOOLNOT(x) -> NOT (ROUND(x, 0))
2655                let rounded = Expression::Function(Box::new(Function::new(
2656                    "ROUND".to_string(),
2657                    vec![arg, Expression::number(0)],
2658                )));
2659                Ok(Expression::Not(Box::new(crate::expressions::UnaryOp {
2660                    this: Expression::Paren(Box::new(Paren {
2661                        this: rounded,
2662                        trailing_comments: Vec::new(),
2663                    })),
2664                    inferred_type: None,
2665                })))
2666            }
2667            "BITMAP_BIT_POSITION" if f.args.len() == 1 => {
2668                let n = f.args.into_iter().next().unwrap();
2669                let case_expr = Expression::Case(Box::new(Case {
2670                    operand: None,
2671                    whens: vec![(
2672                        Expression::Gt(Box::new(BinaryOp {
2673                            left: n.clone(),
2674                            right: Expression::number(0),
2675                            left_comments: Vec::new(),
2676                            operator_comments: Vec::new(),
2677                            trailing_comments: Vec::new(),
2678                            inferred_type: None,
2679                        })),
2680                        Expression::Sub(Box::new(BinaryOp {
2681                            left: n.clone(),
2682                            right: Expression::number(1),
2683                            left_comments: Vec::new(),
2684                            operator_comments: Vec::new(),
2685                            trailing_comments: Vec::new(),
2686                            inferred_type: None,
2687                        })),
2688                    )],
2689                    else_: Some(Expression::Abs(Box::new(UnaryFunc {
2690                        this: n,
2691                        original_name: None,
2692                        inferred_type: None,
2693                    }))),
2694                    comments: Vec::new(),
2695                    inferred_type: None,
2696                }));
2697                Ok(Expression::Mod(Box::new(BinaryOp {
2698                    left: Expression::Paren(Box::new(Paren {
2699                        this: case_expr,
2700                        trailing_comments: Vec::new(),
2701                    })),
2702                    right: Expression::number(32768),
2703                    left_comments: Vec::new(),
2704                    operator_comments: Vec::new(),
2705                    trailing_comments: Vec::new(),
2706                    inferred_type: None,
2707                })))
2708            }
2709            // GREATEST/LEAST - pass through (null-wrapping is handled by source dialect transforms)
2710            "GREATEST" | "LEAST" => Ok(Expression::Function(Box::new(f))),
2711            "GREATEST_IGNORE_NULLS" => Ok(Expression::Greatest(Box::new(VarArgFunc {
2712                expressions: f.args,
2713                original_name: None,
2714                inferred_type: None,
2715            }))),
2716            "LEAST_IGNORE_NULLS" => Ok(Expression::Least(Box::new(VarArgFunc {
2717                expressions: f.args,
2718                original_name: None,
2719                inferred_type: None,
2720            }))),
2721            "PARSE_JSON" => Ok(Expression::Function(Box::new(Function::new(
2722                "JSON".to_string(),
2723                f.args,
2724            )))),
2725            // TRY_PARSE_JSON(x) -> CASE WHEN JSON_VALID(x) THEN CAST(x AS JSON) ELSE NULL END
2726            "TRY_PARSE_JSON" if f.args.len() == 1 => {
2727                let x = f.args.into_iter().next().unwrap();
2728                let json_valid = Expression::Function(Box::new(Function::new(
2729                    "JSON_VALID".to_string(),
2730                    vec![x.clone()],
2731                )));
2732                let cast_json = Expression::Cast(Box::new(crate::expressions::Cast {
2733                    this: x,
2734                    to: DataType::Json,
2735                    double_colon_syntax: false,
2736                    trailing_comments: Vec::new(),
2737                    format: None,
2738                    default: None,
2739                    inferred_type: None,
2740                }));
2741                Ok(Expression::Case(Box::new(crate::expressions::Case {
2742                    operand: None,
2743                    whens: vec![(json_valid, cast_json)],
2744                    else_: Some(Expression::Null(crate::expressions::Null)),
2745                    comments: Vec::new(),
2746                    inferred_type: None,
2747                })))
2748            }
2749            "OBJECT_CONSTRUCT_KEEP_NULL" => {
2750                // OBJECT_CONSTRUCT_KEEP_NULL -> JSON_OBJECT (preserves NULLs)
2751                Ok(Expression::Function(Box::new(Function::new(
2752                    "JSON_OBJECT".to_string(),
2753                    f.args,
2754                ))))
2755            }
2756            "OBJECT_CONSTRUCT" => {
2757                // Convert to DuckDB struct literal: {'key1': val1, 'key2': val2}
2758                let args = f.args;
2759                if args.is_empty() {
2760                    // Empty OBJECT_CONSTRUCT() -> STRUCT_PACK() (no args)
2761                    Ok(Expression::Function(Box::new(Function::new(
2762                        "STRUCT_PACK".to_string(),
2763                        vec![],
2764                    ))))
2765                } else {
2766                    // Build struct literal from key-value pairs
2767                    let mut fields = Vec::new();
2768                    let mut i = 0;
2769                    while i + 1 < args.len() {
2770                        let key = &args[i];
2771                        let value = args[i + 1].clone();
2772                        let key_name = match key {
2773                            Expression::Literal(lit)
2774                                if matches!(lit.as_ref(), Literal::String(_)) =>
2775                            {
2776                                let Literal::String(s) = lit.as_ref() else {
2777                                    unreachable!()
2778                                };
2779                                Some(s.clone())
2780                            }
2781                            _ => None,
2782                        };
2783                        fields.push((key_name, value));
2784                        i += 2;
2785                    }
2786                    Ok(Expression::Struct(Box::new(Struct { fields })))
2787                }
2788            }
2789            "IS_NULL_VALUE" if f.args.len() == 1 => {
2790                let arg = f.args.into_iter().next().unwrap();
2791                Ok(Expression::Eq(Box::new(BinaryOp {
2792                    left: Expression::Function(Box::new(Function::new(
2793                        "JSON_TYPE".to_string(),
2794                        vec![arg],
2795                    ))),
2796                    right: Expression::Literal(Box::new(Literal::String("NULL".to_string()))),
2797                    left_comments: Vec::new(),
2798                    operator_comments: Vec::new(),
2799                    trailing_comments: Vec::new(),
2800                    inferred_type: None,
2801                })))
2802            }
2803            "TRY_TO_DOUBLE" | "TRY_TO_NUMBER" | "TRY_TO_NUMERIC" | "TRY_TO_DECIMAL"
2804                if f.args.len() == 1 =>
2805            {
2806                let arg = f.args.into_iter().next().unwrap();
2807                Ok(Expression::TryCast(Box::new(Cast {
2808                    this: arg,
2809                    to: DataType::Double {
2810                        precision: None,
2811                        scale: None,
2812                    },
2813                    trailing_comments: Vec::new(),
2814                    double_colon_syntax: false,
2815                    format: None,
2816                    default: None,
2817                    inferred_type: None,
2818                })))
2819            }
2820            "TRY_TO_TIME" if f.args.len() == 1 => {
2821                let arg = f.args.into_iter().next().unwrap();
2822                Ok(Expression::TryCast(Box::new(Cast {
2823                    this: arg,
2824                    to: DataType::Time {
2825                        precision: None,
2826                        timezone: false,
2827                    },
2828                    trailing_comments: Vec::new(),
2829                    double_colon_syntax: false,
2830                    format: None,
2831                    default: None,
2832                    inferred_type: None,
2833                })))
2834            }
2835            "TRY_TO_TIME" if f.args.len() == 2 => {
2836                let mut args = f.args;
2837                let value = args.remove(0);
2838                let fmt = self.convert_snowflake_time_format(args.remove(0));
2839                Ok(Expression::TryCast(Box::new(Cast {
2840                    this: Expression::Function(Box::new(Function::new(
2841                        "TRY_STRPTIME".to_string(),
2842                        vec![value, fmt],
2843                    ))),
2844                    to: DataType::Time {
2845                        precision: None,
2846                        timezone: false,
2847                    },
2848                    trailing_comments: Vec::new(),
2849                    double_colon_syntax: false,
2850                    format: None,
2851                    default: None,
2852                    inferred_type: None,
2853                })))
2854            }
2855            "TRY_TO_TIMESTAMP" if f.args.len() == 1 => {
2856                let arg = f.args.into_iter().next().unwrap();
2857                Ok(Expression::TryCast(Box::new(Cast {
2858                    this: arg,
2859                    to: DataType::Timestamp {
2860                        precision: None,
2861                        timezone: false,
2862                    },
2863                    trailing_comments: Vec::new(),
2864                    double_colon_syntax: false,
2865                    format: None,
2866                    default: None,
2867                    inferred_type: None,
2868                })))
2869            }
2870            "TRY_TO_TIMESTAMP" if f.args.len() == 2 => {
2871                let mut args = f.args;
2872                let value = args.remove(0);
2873                let fmt = self.convert_snowflake_time_format(args.remove(0));
2874                Ok(Expression::Cast(Box::new(Cast {
2875                    this: Expression::Function(Box::new(Function::new(
2876                        "TRY_STRPTIME".to_string(),
2877                        vec![value, fmt],
2878                    ))),
2879                    to: DataType::Timestamp {
2880                        precision: None,
2881                        timezone: false,
2882                    },
2883                    trailing_comments: Vec::new(),
2884                    double_colon_syntax: false,
2885                    format: None,
2886                    default: None,
2887                    inferred_type: None,
2888                })))
2889            }
2890            "TRY_TO_DATE" if f.args.len() == 1 => {
2891                let arg = f.args.into_iter().next().unwrap();
2892                Ok(Expression::TryCast(Box::new(Cast {
2893                    this: arg,
2894                    to: DataType::Date,
2895                    trailing_comments: Vec::new(),
2896                    double_colon_syntax: false,
2897                    format: None,
2898                    default: None,
2899                    inferred_type: None,
2900                })))
2901            }
2902            "DAYOFWEEKISO" | "DAYOFWEEK_ISO" => Ok(Expression::Function(Box::new(Function::new(
2903                "ISODOW".to_string(),
2904                f.args,
2905            )))),
2906            "YEAROFWEEK" | "YEAROFWEEKISO" if f.args.len() == 1 => {
2907                let arg = f.args.into_iter().next().unwrap();
2908                Ok(Expression::Extract(Box::new(
2909                    crate::expressions::ExtractFunc {
2910                        this: arg,
2911                        field: crate::expressions::DateTimeField::Custom("ISOYEAR".to_string()),
2912                    },
2913                )))
2914            }
2915            "WEEKISO" => Ok(Expression::Function(Box::new(Function::new(
2916                "WEEKOFYEAR".to_string(),
2917                f.args,
2918            )))),
2919            "TIME_FROM_PARTS" | "TIMEFROMPARTS" if f.args.len() == 3 => {
2920                let args_ref = &f.args;
2921                // Check if all args are in-range literals: h < 24, m < 60, s < 60
2922                let all_in_range = if let (Some(h_val), Some(m_val), Some(s_val)) = (
2923                    Self::extract_number_value(&args_ref[0]),
2924                    Self::extract_number_value(&args_ref[1]),
2925                    Self::extract_number_value(&args_ref[2]),
2926                ) {
2927                    h_val >= 0.0
2928                        && h_val < 24.0
2929                        && m_val >= 0.0
2930                        && m_val < 60.0
2931                        && s_val >= 0.0
2932                        && s_val < 60.0
2933                } else {
2934                    false
2935                };
2936                if all_in_range {
2937                    // Use MAKE_TIME for normal values
2938                    Ok(Expression::Function(Box::new(Function::new(
2939                        "MAKE_TIME".to_string(),
2940                        f.args,
2941                    ))))
2942                } else {
2943                    // TIME_FROM_PARTS(h, m, s) -> CAST('00:00:00' AS TIME) + INTERVAL ((h * 3600) + (m * 60) + s) SECOND
2944                    // Use arithmetic approach to handle out-of-range values (e.g., 100 minutes)
2945                    let mut args = f.args;
2946                    let h = args.remove(0);
2947                    let m = args.remove(0);
2948                    let s = args.remove(0);
2949                    let seconds_expr = Expression::Add(Box::new(BinaryOp {
2950                        left: Expression::Add(Box::new(BinaryOp {
2951                            left: Expression::Paren(Box::new(Paren {
2952                                this: Expression::Mul(Box::new(BinaryOp {
2953                                    left: h,
2954                                    right: Expression::number(3600),
2955                                    left_comments: Vec::new(),
2956                                    operator_comments: Vec::new(),
2957                                    trailing_comments: Vec::new(),
2958                                    inferred_type: None,
2959                                })),
2960                                trailing_comments: Vec::new(),
2961                            })),
2962                            right: Expression::Paren(Box::new(Paren {
2963                                this: Expression::Mul(Box::new(BinaryOp {
2964                                    left: m,
2965                                    right: Expression::number(60),
2966                                    left_comments: Vec::new(),
2967                                    operator_comments: Vec::new(),
2968                                    trailing_comments: Vec::new(),
2969                                    inferred_type: None,
2970                                })),
2971                                trailing_comments: Vec::new(),
2972                            })),
2973                            left_comments: Vec::new(),
2974                            operator_comments: Vec::new(),
2975                            trailing_comments: Vec::new(),
2976                            inferred_type: None,
2977                        })),
2978                        right: s,
2979                        left_comments: Vec::new(),
2980                        operator_comments: Vec::new(),
2981                        trailing_comments: Vec::new(),
2982                        inferred_type: None,
2983                    }));
2984                    let base_time = Expression::Cast(Box::new(Cast {
2985                        this: Expression::Literal(Box::new(Literal::String(
2986                            "00:00:00".to_string(),
2987                        ))),
2988                        to: DataType::Time {
2989                            precision: None,
2990                            timezone: false,
2991                        },
2992                        trailing_comments: Vec::new(),
2993                        double_colon_syntax: false,
2994                        format: None,
2995                        default: None,
2996                        inferred_type: None,
2997                    }));
2998                    Ok(Expression::Add(Box::new(BinaryOp {
2999                        left: base_time,
3000                        right: Expression::Interval(Box::new(Interval {
3001                            this: Some(Expression::Paren(Box::new(crate::expressions::Paren {
3002                                this: seconds_expr,
3003                                trailing_comments: Vec::new(),
3004                            }))),
3005                            unit: Some(IntervalUnitSpec::Simple {
3006                                unit: IntervalUnit::Second,
3007                                use_plural: false,
3008                            }),
3009                        })),
3010                        left_comments: Vec::new(),
3011                        operator_comments: Vec::new(),
3012                        trailing_comments: Vec::new(),
3013                        inferred_type: None,
3014                    })))
3015                }
3016            }
3017            "TIME_FROM_PARTS" | "TIMEFROMPARTS" if f.args.len() == 4 => {
3018                let mut args = f.args;
3019                let h = args.remove(0);
3020                let m = args.remove(0);
3021                let s = args.remove(0);
3022                let ns = args.remove(0);
3023                let seconds_expr = Expression::Add(Box::new(BinaryOp {
3024                    left: Expression::Add(Box::new(BinaryOp {
3025                        left: Expression::Add(Box::new(BinaryOp {
3026                            left: Expression::Paren(Box::new(Paren {
3027                                this: Expression::Mul(Box::new(BinaryOp {
3028                                    left: h,
3029                                    right: Expression::number(3600),
3030                                    left_comments: Vec::new(),
3031                                    operator_comments: Vec::new(),
3032                                    trailing_comments: Vec::new(),
3033                                    inferred_type: None,
3034                                })),
3035                                trailing_comments: Vec::new(),
3036                            })),
3037                            right: Expression::Paren(Box::new(Paren {
3038                                this: Expression::Mul(Box::new(BinaryOp {
3039                                    left: m,
3040                                    right: Expression::number(60),
3041                                    left_comments: Vec::new(),
3042                                    operator_comments: Vec::new(),
3043                                    trailing_comments: Vec::new(),
3044                                    inferred_type: None,
3045                                })),
3046                                trailing_comments: Vec::new(),
3047                            })),
3048                            left_comments: Vec::new(),
3049                            operator_comments: Vec::new(),
3050                            trailing_comments: Vec::new(),
3051                            inferred_type: None,
3052                        })),
3053                        right: s,
3054                        left_comments: Vec::new(),
3055                        operator_comments: Vec::new(),
3056                        trailing_comments: Vec::new(),
3057                        inferred_type: None,
3058                    })),
3059                    right: Expression::Paren(Box::new(Paren {
3060                        this: Expression::Div(Box::new(BinaryOp {
3061                            left: ns,
3062                            right: Expression::Literal(Box::new(Literal::Number(
3063                                "1000000000.0".to_string(),
3064                            ))),
3065                            left_comments: Vec::new(),
3066                            operator_comments: Vec::new(),
3067                            trailing_comments: Vec::new(),
3068                            inferred_type: None,
3069                        })),
3070                        trailing_comments: Vec::new(),
3071                    })),
3072                    left_comments: Vec::new(),
3073                    operator_comments: Vec::new(),
3074                    trailing_comments: Vec::new(),
3075                    inferred_type: None,
3076                }));
3077                let base_time = Expression::Cast(Box::new(Cast {
3078                    this: Expression::Literal(Box::new(Literal::String("00:00:00".to_string()))),
3079                    to: DataType::Time {
3080                        precision: None,
3081                        timezone: false,
3082                    },
3083                    trailing_comments: Vec::new(),
3084                    double_colon_syntax: false,
3085                    format: None,
3086                    default: None,
3087                    inferred_type: None,
3088                }));
3089                Ok(Expression::Add(Box::new(BinaryOp {
3090                    left: base_time,
3091                    right: Expression::Interval(Box::new(Interval {
3092                        this: Some(Expression::Paren(Box::new(crate::expressions::Paren {
3093                            this: seconds_expr,
3094                            trailing_comments: Vec::new(),
3095                        }))),
3096                        unit: Some(IntervalUnitSpec::Simple {
3097                            unit: IntervalUnit::Second,
3098                            use_plural: false,
3099                        }),
3100                    })),
3101                    left_comments: Vec::new(),
3102                    operator_comments: Vec::new(),
3103                    trailing_comments: Vec::new(),
3104                    inferred_type: None,
3105                })))
3106            }
3107            "TIMESTAMP_FROM_PARTS" | "TIMESTAMPFROMPARTS" if f.args.len() == 6 => {
3108                Ok(Expression::Function(Box::new(Function::new(
3109                    "MAKE_TIMESTAMP".to_string(),
3110                    f.args,
3111                ))))
3112            }
3113            "TIMESTAMP_FROM_PARTS" | "TIMESTAMPFROMPARTS" | "TIMESTAMP_NTZ_FROM_PARTS"
3114                if f.args.len() == 2 =>
3115            {
3116                let mut args = f.args;
3117                let d = args.remove(0);
3118                let t = args.remove(0);
3119                Ok(Expression::Add(Box::new(BinaryOp {
3120                    left: d,
3121                    right: t,
3122                    left_comments: Vec::new(),
3123                    operator_comments: Vec::new(),
3124                    trailing_comments: Vec::new(),
3125                    inferred_type: None,
3126                })))
3127            }
3128            "TIMESTAMP_LTZ_FROM_PARTS" if f.args.len() == 6 => {
3129                Ok(Expression::Cast(Box::new(Cast {
3130                    this: Expression::Function(Box::new(Function::new(
3131                        "MAKE_TIMESTAMP".to_string(),
3132                        f.args,
3133                    ))),
3134                    to: DataType::Timestamp {
3135                        precision: None,
3136                        timezone: true,
3137                    },
3138                    trailing_comments: Vec::new(),
3139                    double_colon_syntax: false,
3140                    format: None,
3141                    default: None,
3142                    inferred_type: None,
3143                })))
3144            }
3145            "TIMESTAMP_TZ_FROM_PARTS" if f.args.len() == 8 => {
3146                let mut args = f.args;
3147                let ts_args = vec![
3148                    args.remove(0),
3149                    args.remove(0),
3150                    args.remove(0),
3151                    args.remove(0),
3152                    args.remove(0),
3153                    args.remove(0),
3154                ];
3155                let _nano = args.remove(0);
3156                let tz = args.remove(0);
3157                Ok(Expression::AtTimeZone(Box::new(
3158                    crate::expressions::AtTimeZone {
3159                        this: Expression::Function(Box::new(Function::new(
3160                            "MAKE_TIMESTAMP".to_string(),
3161                            ts_args,
3162                        ))),
3163                        zone: tz,
3164                    },
3165                )))
3166            }
3167            "BOOLAND_AGG" if f.args.len() == 1 => {
3168                let arg = f.args.into_iter().next().unwrap();
3169                Ok(Expression::Function(Box::new(Function::new(
3170                    "BOOL_AND".to_string(),
3171                    vec![Expression::Cast(Box::new(Cast {
3172                        this: arg,
3173                        to: DataType::Boolean,
3174                        trailing_comments: Vec::new(),
3175                        double_colon_syntax: false,
3176                        format: None,
3177                        default: None,
3178                        inferred_type: None,
3179                    }))],
3180                ))))
3181            }
3182            "BOOLOR_AGG" if f.args.len() == 1 => {
3183                let arg = f.args.into_iter().next().unwrap();
3184                Ok(Expression::Function(Box::new(Function::new(
3185                    "BOOL_OR".to_string(),
3186                    vec![Expression::Cast(Box::new(Cast {
3187                        this: arg,
3188                        to: DataType::Boolean,
3189                        trailing_comments: Vec::new(),
3190                        double_colon_syntax: false,
3191                        format: None,
3192                        default: None,
3193                        inferred_type: None,
3194                    }))],
3195                ))))
3196            }
3197            "NVL2" if f.args.len() == 3 => {
3198                let mut args = f.args;
3199                let a = args.remove(0);
3200                let b = args.remove(0);
3201                let c = args.remove(0);
3202                Ok(Expression::Case(Box::new(Case {
3203                    operand: None,
3204                    whens: vec![(
3205                        Expression::Not(Box::new(crate::expressions::UnaryOp {
3206                            this: Expression::IsNull(Box::new(crate::expressions::IsNull {
3207                                this: a,
3208                                not: false,
3209                                postfix_form: false,
3210                            })),
3211                            inferred_type: None,
3212                        })),
3213                        b,
3214                    )],
3215                    else_: Some(c),
3216                    comments: Vec::new(),
3217                    inferred_type: None,
3218                })))
3219            }
3220            "EQUAL_NULL" if f.args.len() == 2 => {
3221                let mut args = f.args;
3222                let a = args.remove(0);
3223                let b = args.remove(0);
3224                Ok(Expression::NullSafeEq(Box::new(BinaryOp {
3225                    left: a,
3226                    right: b,
3227                    left_comments: Vec::new(),
3228                    operator_comments: Vec::new(),
3229                    trailing_comments: Vec::new(),
3230                    inferred_type: None,
3231                })))
3232            }
3233            "EDITDISTANCE" if f.args.len() == 3 => {
3234                // EDITDISTANCE(a, b, max) -> CASE WHEN LEVENSHTEIN(a, b) IS NULL OR max IS NULL THEN NULL ELSE LEAST(LEVENSHTEIN(a, b), max) END
3235                let mut args = f.args;
3236                let a = args.remove(0);
3237                let b = args.remove(0);
3238                let max_dist = args.remove(0);
3239                let lev = Expression::Function(Box::new(Function::new(
3240                    "LEVENSHTEIN".to_string(),
3241                    vec![a, b],
3242                )));
3243                let lev_is_null = Expression::IsNull(Box::new(crate::expressions::IsNull {
3244                    this: lev.clone(),
3245                    not: false,
3246                    postfix_form: false,
3247                }));
3248                let max_is_null = Expression::IsNull(Box::new(crate::expressions::IsNull {
3249                    this: max_dist.clone(),
3250                    not: false,
3251                    postfix_form: false,
3252                }));
3253                let null_check = Expression::Or(Box::new(BinaryOp {
3254                    left: lev_is_null,
3255                    right: max_is_null,
3256                    left_comments: Vec::new(),
3257                    operator_comments: Vec::new(),
3258                    trailing_comments: Vec::new(),
3259                    inferred_type: None,
3260                }));
3261                let least = Expression::Least(Box::new(VarArgFunc {
3262                    expressions: vec![lev, max_dist],
3263                    original_name: None,
3264                    inferred_type: None,
3265                }));
3266                Ok(Expression::Case(Box::new(Case {
3267                    operand: None,
3268                    whens: vec![(null_check, Expression::Null(crate::expressions::Null))],
3269                    else_: Some(least),
3270                    comments: Vec::new(),
3271                    inferred_type: None,
3272                })))
3273            }
3274            "EDITDISTANCE" => Ok(Expression::Function(Box::new(Function::new(
3275                "LEVENSHTEIN".to_string(),
3276                f.args,
3277            )))),
3278            "BITAND" if f.args.len() == 2 => {
3279                let mut args = f.args;
3280                let left = args.remove(0);
3281                let right = args.remove(0);
3282                // Wrap shift expressions in parentheses for correct precedence
3283                let wrap = |e: Expression| -> Expression {
3284                    match &e {
3285                        Expression::BitwiseLeftShift(_) | Expression::BitwiseRightShift(_) => {
3286                            Expression::Paren(Box::new(Paren {
3287                                this: e,
3288                                trailing_comments: Vec::new(),
3289                            }))
3290                        }
3291                        _ => e,
3292                    }
3293                };
3294                Ok(Expression::BitwiseAnd(Box::new(BinaryOp {
3295                    left: wrap(left),
3296                    right: wrap(right),
3297                    left_comments: Vec::new(),
3298                    operator_comments: Vec::new(),
3299                    trailing_comments: Vec::new(),
3300                    inferred_type: None,
3301                })))
3302            }
3303            "BITOR" if f.args.len() == 2 => {
3304                let mut args = f.args;
3305                let left = args.remove(0);
3306                let right = args.remove(0);
3307                // Wrap shift expressions in parentheses for correct precedence
3308                let wrap = |e: Expression| -> Expression {
3309                    match &e {
3310                        Expression::BitwiseLeftShift(_) | Expression::BitwiseRightShift(_) => {
3311                            Expression::Paren(Box::new(Paren {
3312                                this: e,
3313                                trailing_comments: Vec::new(),
3314                            }))
3315                        }
3316                        _ => e,
3317                    }
3318                };
3319                Ok(Expression::BitwiseOr(Box::new(BinaryOp {
3320                    left: wrap(left),
3321                    right: wrap(right),
3322                    left_comments: Vec::new(),
3323                    operator_comments: Vec::new(),
3324                    trailing_comments: Vec::new(),
3325                    inferred_type: None,
3326                })))
3327            }
3328            "BITXOR" if f.args.len() == 2 => {
3329                let mut args = f.args;
3330                Ok(Expression::BitwiseXor(Box::new(BinaryOp {
3331                    left: args.remove(0),
3332                    right: args.remove(0),
3333                    left_comments: Vec::new(),
3334                    operator_comments: Vec::new(),
3335                    trailing_comments: Vec::new(),
3336                    inferred_type: None,
3337                })))
3338            }
3339            "BITNOT" if f.args.len() == 1 => {
3340                let arg = f.args.into_iter().next().unwrap();
3341                Ok(Expression::BitwiseNot(Box::new(
3342                    crate::expressions::UnaryOp {
3343                        this: Expression::Paren(Box::new(Paren {
3344                            this: arg,
3345                            trailing_comments: Vec::new(),
3346                        })),
3347                        inferred_type: None,
3348                    },
3349                )))
3350            }
3351            "BITSHIFTLEFT" if f.args.len() == 2 => {
3352                let mut args = f.args;
3353                let a = args.remove(0);
3354                let b = args.remove(0);
3355                // Check if first arg is BINARY/BLOB type (e.g., X'002A'::BINARY)
3356                let is_binary = if let Expression::Cast(ref c) = a {
3357                    matches!(
3358                        &c.to,
3359                        DataType::Binary { .. } | DataType::VarBinary { .. } | DataType::Blob
3360                    ) || matches!(&c.to, DataType::Custom { name } if name == "BLOB")
3361                } else {
3362                    false
3363                };
3364                if is_binary {
3365                    // CAST(CAST(a AS BIT) << b AS BLOB)
3366                    let cast_to_bit = Expression::Cast(Box::new(Cast {
3367                        this: a,
3368                        to: DataType::Custom {
3369                            name: "BIT".to_string(),
3370                        },
3371                        trailing_comments: Vec::new(),
3372                        double_colon_syntax: false,
3373                        format: None,
3374                        default: None,
3375                        inferred_type: None,
3376                    }));
3377                    let shift = Expression::BitwiseLeftShift(Box::new(BinaryOp {
3378                        left: cast_to_bit,
3379                        right: b,
3380                        left_comments: Vec::new(),
3381                        operator_comments: Vec::new(),
3382                        trailing_comments: Vec::new(),
3383                        inferred_type: None,
3384                    }));
3385                    Ok(Expression::Cast(Box::new(Cast {
3386                        this: shift,
3387                        to: DataType::Custom {
3388                            name: "BLOB".to_string(),
3389                        },
3390                        trailing_comments: Vec::new(),
3391                        double_colon_syntax: false,
3392                        format: None,
3393                        default: None,
3394                        inferred_type: None,
3395                    })))
3396                } else {
3397                    Ok(Expression::BitwiseLeftShift(Box::new(BinaryOp {
3398                        left: Expression::Cast(Box::new(Cast {
3399                            this: a,
3400                            to: DataType::Int128,
3401                            trailing_comments: Vec::new(),
3402                            double_colon_syntax: false,
3403                            format: None,
3404                            default: None,
3405                            inferred_type: None,
3406                        })),
3407                        right: b,
3408                        left_comments: Vec::new(),
3409                        operator_comments: Vec::new(),
3410                        trailing_comments: Vec::new(),
3411                        inferred_type: None,
3412                    })))
3413                }
3414            }
3415            "BITSHIFTRIGHT" if f.args.len() == 2 => {
3416                let mut args = f.args;
3417                let a = args.remove(0);
3418                let b = args.remove(0);
3419                // Check if first arg is BINARY/BLOB type (e.g., X'002A'::BINARY)
3420                let is_binary = if let Expression::Cast(ref c) = a {
3421                    matches!(
3422                        &c.to,
3423                        DataType::Binary { .. } | DataType::VarBinary { .. } | DataType::Blob
3424                    ) || matches!(&c.to, DataType::Custom { name } if name == "BLOB")
3425                } else {
3426                    false
3427                };
3428                if is_binary {
3429                    // CAST(CAST(a AS BIT) >> b AS BLOB)
3430                    let cast_to_bit = Expression::Cast(Box::new(Cast {
3431                        this: a,
3432                        to: DataType::Custom {
3433                            name: "BIT".to_string(),
3434                        },
3435                        trailing_comments: Vec::new(),
3436                        double_colon_syntax: false,
3437                        format: None,
3438                        default: None,
3439                        inferred_type: None,
3440                    }));
3441                    let shift = Expression::BitwiseRightShift(Box::new(BinaryOp {
3442                        left: cast_to_bit,
3443                        right: b,
3444                        left_comments: Vec::new(),
3445                        operator_comments: Vec::new(),
3446                        trailing_comments: Vec::new(),
3447                        inferred_type: None,
3448                    }));
3449                    Ok(Expression::Cast(Box::new(Cast {
3450                        this: shift,
3451                        to: DataType::Custom {
3452                            name: "BLOB".to_string(),
3453                        },
3454                        trailing_comments: Vec::new(),
3455                        double_colon_syntax: false,
3456                        format: None,
3457                        default: None,
3458                        inferred_type: None,
3459                    })))
3460                } else {
3461                    Ok(Expression::BitwiseRightShift(Box::new(BinaryOp {
3462                        left: Expression::Cast(Box::new(Cast {
3463                            this: a,
3464                            to: DataType::Int128,
3465                            trailing_comments: Vec::new(),
3466                            double_colon_syntax: false,
3467                            format: None,
3468                            default: None,
3469                            inferred_type: None,
3470                        })),
3471                        right: b,
3472                        left_comments: Vec::new(),
3473                        operator_comments: Vec::new(),
3474                        trailing_comments: Vec::new(),
3475                        inferred_type: None,
3476                    })))
3477                }
3478            }
3479            "SQUARE" if f.args.len() == 1 => {
3480                let arg = f.args.into_iter().next().unwrap();
3481                Ok(Expression::Function(Box::new(Function::new(
3482                    "POWER".to_string(),
3483                    vec![arg, Expression::number(2)],
3484                ))))
3485            }
3486            "LIST"
3487                if f.args.len() == 1 && !matches!(f.args.first(), Some(Expression::Select(_))) =>
3488            {
3489                Ok(Expression::Function(Box::new(Function::new(
3490                    "ARRAY_AGG".to_string(),
3491                    f.args,
3492                ))))
3493            }
3494            "UUID_STRING" => {
3495                if f.args.is_empty() {
3496                    Ok(Expression::Function(Box::new(Function::new(
3497                        "UUID".to_string(),
3498                        vec![],
3499                    ))))
3500                } else {
3501                    Ok(Expression::Function(Box::new(Function::new(
3502                        "UUID_STRING".to_string(),
3503                        f.args,
3504                    ))))
3505                }
3506            }
3507            "ENDSWITH" => Ok(Expression::Function(Box::new(Function::new(
3508                "ENDS_WITH".to_string(),
3509                f.args,
3510            )))),
3511            // REGEXP_REPLACE: 'g' flag is handled by cross_dialect_normalize for source dialects
3512            // that default to global replacement (e.g., Snowflake). DuckDB defaults to first-match,
3513            // so no 'g' flag needed for DuckDB identity or PostgreSQL->DuckDB.
3514            "REGEXP_REPLACE" if f.args.len() == 2 => {
3515                // 2-arg form (subject, pattern) -> add empty replacement
3516                let mut args = f.args;
3517                args.push(Expression::Literal(Box::new(
3518                    Literal::String(String::new()),
3519                )));
3520                Ok(Expression::Function(Box::new(Function::new(
3521                    "REGEXP_REPLACE".to_string(),
3522                    args,
3523                ))))
3524            }
3525            "DIV0" if f.args.len() == 2 => {
3526                let mut args = f.args;
3527                let a = args.remove(0);
3528                let b = args.remove(0);
3529                Ok(Expression::Case(Box::new(Case {
3530                    operand: None,
3531                    whens: vec![(
3532                        Expression::And(Box::new(BinaryOp {
3533                            left: Expression::Eq(Box::new(BinaryOp {
3534                                left: b.clone(),
3535                                right: Expression::number(0),
3536                                left_comments: Vec::new(),
3537                                operator_comments: Vec::new(),
3538                                trailing_comments: Vec::new(),
3539                                inferred_type: None,
3540                            })),
3541                            right: Expression::Not(Box::new(crate::expressions::UnaryOp {
3542                                this: Expression::IsNull(Box::new(crate::expressions::IsNull {
3543                                    this: a.clone(),
3544                                    not: false,
3545                                    postfix_form: false,
3546                                })),
3547                                inferred_type: None,
3548                            })),
3549                            left_comments: Vec::new(),
3550                            operator_comments: Vec::new(),
3551                            trailing_comments: Vec::new(),
3552                            inferred_type: None,
3553                        })),
3554                        Expression::number(0),
3555                    )],
3556                    else_: Some(Expression::Div(Box::new(BinaryOp {
3557                        left: a,
3558                        right: b,
3559                        left_comments: Vec::new(),
3560                        operator_comments: Vec::new(),
3561                        trailing_comments: Vec::new(),
3562                        inferred_type: None,
3563                    }))),
3564                    comments: Vec::new(),
3565                    inferred_type: None,
3566                })))
3567            }
3568            "DIV0NULL" if f.args.len() == 2 => {
3569                let mut args = f.args;
3570                let a = args.remove(0);
3571                let b = args.remove(0);
3572                Ok(Expression::Case(Box::new(Case {
3573                    operand: None,
3574                    whens: vec![(
3575                        Expression::Or(Box::new(BinaryOp {
3576                            left: Expression::Eq(Box::new(BinaryOp {
3577                                left: b.clone(),
3578                                right: Expression::number(0),
3579                                left_comments: Vec::new(),
3580                                operator_comments: Vec::new(),
3581                                trailing_comments: Vec::new(),
3582                                inferred_type: None,
3583                            })),
3584                            right: Expression::IsNull(Box::new(crate::expressions::IsNull {
3585                                this: b.clone(),
3586                                not: false,
3587                                postfix_form: false,
3588                            })),
3589                            left_comments: Vec::new(),
3590                            operator_comments: Vec::new(),
3591                            trailing_comments: Vec::new(),
3592                            inferred_type: None,
3593                        })),
3594                        Expression::number(0),
3595                    )],
3596                    else_: Some(Expression::Div(Box::new(BinaryOp {
3597                        left: a,
3598                        right: b,
3599                        left_comments: Vec::new(),
3600                        operator_comments: Vec::new(),
3601                        trailing_comments: Vec::new(),
3602                        inferred_type: None,
3603                    }))),
3604                    comments: Vec::new(),
3605                    inferred_type: None,
3606                })))
3607            }
3608            "ZEROIFNULL" if f.args.len() == 1 => {
3609                let x = f.args.into_iter().next().unwrap();
3610                Ok(Expression::Case(Box::new(Case {
3611                    operand: None,
3612                    whens: vec![(
3613                        Expression::IsNull(Box::new(crate::expressions::IsNull {
3614                            this: x.clone(),
3615                            not: false,
3616                            postfix_form: false,
3617                        })),
3618                        Expression::number(0),
3619                    )],
3620                    else_: Some(x),
3621                    comments: Vec::new(),
3622                    inferred_type: None,
3623                })))
3624            }
3625            "NULLIFZERO" if f.args.len() == 1 => {
3626                let x = f.args.into_iter().next().unwrap();
3627                Ok(Expression::Case(Box::new(Case {
3628                    operand: None,
3629                    whens: vec![(
3630                        Expression::Eq(Box::new(BinaryOp {
3631                            left: x.clone(),
3632                            right: Expression::number(0),
3633                            left_comments: Vec::new(),
3634                            operator_comments: Vec::new(),
3635                            trailing_comments: Vec::new(),
3636                            inferred_type: None,
3637                        })),
3638                        Expression::Null(crate::expressions::Null),
3639                    )],
3640                    else_: Some(x),
3641                    comments: Vec::new(),
3642                    inferred_type: None,
3643                })))
3644            }
3645            "TO_DOUBLE" if f.args.len() == 1 => {
3646                let arg = f.args.into_iter().next().unwrap();
3647                Ok(Expression::Cast(Box::new(Cast {
3648                    this: arg,
3649                    to: DataType::Double {
3650                        precision: None,
3651                        scale: None,
3652                    },
3653                    trailing_comments: Vec::new(),
3654                    double_colon_syntax: false,
3655                    format: None,
3656                    default: None,
3657                    inferred_type: None,
3658                })))
3659            }
3660            "DATE" if f.args.len() == 1 => {
3661                let arg = f.args.into_iter().next().unwrap();
3662                Ok(Expression::Cast(Box::new(Cast {
3663                    this: arg,
3664                    to: DataType::Date,
3665                    trailing_comments: Vec::new(),
3666                    double_colon_syntax: false,
3667                    format: None,
3668                    default: None,
3669                    inferred_type: None,
3670                })))
3671            }
3672            "DATE" if f.args.len() == 2 => {
3673                let mut args = f.args;
3674                let value = args.remove(0);
3675                let fmt = self.convert_snowflake_date_format(args.remove(0));
3676                Ok(Expression::Cast(Box::new(Cast {
3677                    this: Expression::Function(Box::new(Function::new(
3678                        "STRPTIME".to_string(),
3679                        vec![value, fmt],
3680                    ))),
3681                    to: DataType::Date,
3682                    trailing_comments: Vec::new(),
3683                    double_colon_syntax: false,
3684                    format: None,
3685                    default: None,
3686                    inferred_type: None,
3687                })))
3688            }
3689            "SYSDATE" => Ok(Expression::AtTimeZone(Box::new(
3690                crate::expressions::AtTimeZone {
3691                    this: Expression::CurrentTimestamp(crate::expressions::CurrentTimestamp {
3692                        precision: None,
3693                        sysdate: false,
3694                    }),
3695                    zone: Expression::Literal(Box::new(Literal::String("UTC".to_string()))),
3696                },
3697            ))),
3698            "HEX_DECODE_BINARY" => Ok(Expression::Function(Box::new(Function::new(
3699                "UNHEX".to_string(),
3700                f.args,
3701            )))),
3702            "CONVERT_TIMEZONE" if f.args.len() == 3 => {
3703                let mut args = f.args;
3704                let src_tz = args.remove(0);
3705                let tgt_tz = args.remove(0);
3706                let ts = args.remove(0);
3707                let cast_ts = Expression::Cast(Box::new(Cast {
3708                    this: ts,
3709                    to: DataType::Timestamp {
3710                        precision: None,
3711                        timezone: false,
3712                    },
3713                    trailing_comments: Vec::new(),
3714                    double_colon_syntax: false,
3715                    format: None,
3716                    default: None,
3717                    inferred_type: None,
3718                }));
3719                Ok(Expression::AtTimeZone(Box::new(
3720                    crate::expressions::AtTimeZone {
3721                        this: Expression::AtTimeZone(Box::new(crate::expressions::AtTimeZone {
3722                            this: cast_ts,
3723                            zone: src_tz,
3724                        })),
3725                        zone: tgt_tz,
3726                    },
3727                )))
3728            }
3729            "CONVERT_TIMEZONE" if f.args.len() == 2 => {
3730                let mut args = f.args;
3731                let tgt_tz = args.remove(0);
3732                let ts = args.remove(0);
3733                let cast_ts = Expression::Cast(Box::new(Cast {
3734                    this: ts,
3735                    to: DataType::Timestamp {
3736                        precision: None,
3737                        timezone: false,
3738                    },
3739                    trailing_comments: Vec::new(),
3740                    double_colon_syntax: false,
3741                    format: None,
3742                    default: None,
3743                    inferred_type: None,
3744                }));
3745                Ok(Expression::AtTimeZone(Box::new(
3746                    crate::expressions::AtTimeZone {
3747                        this: cast_ts,
3748                        zone: tgt_tz,
3749                    },
3750                )))
3751            }
3752            "DATE_PART" | "DATEPART" if f.args.len() == 2 => self.transform_date_part(f.args),
3753            "DATEADD" | "TIMEADD" if f.args.len() == 3 => self.transform_dateadd(f.args),
3754            "TIMESTAMPADD" if f.args.len() == 3 => self.transform_dateadd(f.args),
3755            "DATEDIFF" | "TIMEDIFF" if f.args.len() == 3 => self.transform_datediff(f.args),
3756            "TIMESTAMPDIFF" if f.args.len() == 3 => self.transform_datediff(f.args),
3757            "CORR" if f.args.len() == 2 => {
3758                // DuckDB handles NaN natively - no ISNAN wrapping needed
3759                Ok(Expression::Function(Box::new(f)))
3760            }
3761            "TO_TIMESTAMP" | "TO_TIMESTAMP_NTZ" if f.args.len() == 2 => {
3762                let mut args = f.args;
3763                let value = args.remove(0);
3764                let second_arg = args.remove(0);
3765                match &second_arg {
3766                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => Ok(
3767                        Expression::AtTimeZone(Box::new(crate::expressions::AtTimeZone {
3768                            this: Expression::Function(Box::new(Function::new(
3769                                "TO_TIMESTAMP".to_string(),
3770                                vec![Expression::Div(Box::new(BinaryOp {
3771                                    left: value,
3772                                    right: Expression::Function(Box::new(Function::new(
3773                                        "POWER".to_string(),
3774                                        vec![Expression::number(10), second_arg],
3775                                    ))),
3776                                    left_comments: Vec::new(),
3777                                    operator_comments: Vec::new(),
3778                                    trailing_comments: Vec::new(),
3779                                    inferred_type: None,
3780                                }))],
3781                            ))),
3782                            zone: Expression::Literal(Box::new(Literal::String("UTC".to_string()))),
3783                        })),
3784                    ),
3785                    _ => {
3786                        let fmt = self.convert_snowflake_time_format(second_arg);
3787                        Ok(Expression::Function(Box::new(Function::new(
3788                            "STRPTIME".to_string(),
3789                            vec![value, fmt],
3790                        ))))
3791                    }
3792                }
3793            }
3794            "TO_TIME" if f.args.len() == 1 => {
3795                let arg = f.args.into_iter().next().unwrap();
3796                Ok(Expression::Cast(Box::new(Cast {
3797                    this: arg,
3798                    to: DataType::Time {
3799                        precision: None,
3800                        timezone: false,
3801                    },
3802                    trailing_comments: Vec::new(),
3803                    double_colon_syntax: false,
3804                    format: None,
3805                    default: None,
3806                    inferred_type: None,
3807                })))
3808            }
3809            "TO_TIME" if f.args.len() == 2 => {
3810                let mut args = f.args;
3811                let value = args.remove(0);
3812                let fmt = self.convert_snowflake_time_format(args.remove(0));
3813                Ok(Expression::Cast(Box::new(Cast {
3814                    this: Expression::Function(Box::new(Function::new(
3815                        "STRPTIME".to_string(),
3816                        vec![value, fmt],
3817                    ))),
3818                    to: DataType::Time {
3819                        precision: None,
3820                        timezone: false,
3821                    },
3822                    trailing_comments: Vec::new(),
3823                    double_colon_syntax: false,
3824                    format: None,
3825                    default: None,
3826                    inferred_type: None,
3827                })))
3828            }
3829            "TO_DATE" if f.args.len() == 2 => {
3830                let mut args = f.args;
3831                let value = args.remove(0);
3832                let fmt = self.convert_snowflake_date_format(args.remove(0));
3833                Ok(Expression::Cast(Box::new(Cast {
3834                    this: Expression::Function(Box::new(Function::new(
3835                        "STRPTIME".to_string(),
3836                        vec![value, fmt],
3837                    ))),
3838                    to: DataType::Date,
3839                    trailing_comments: Vec::new(),
3840                    double_colon_syntax: false,
3841                    format: None,
3842                    default: None,
3843                    inferred_type: None,
3844                })))
3845            }
3846            // LAST_DAY with 2 args handled by comprehensive handler below
3847
3848            // SAFE_DIVIDE(x, y) -> CASE WHEN y <> 0 THEN x / y ELSE NULL END
3849            "SAFE_DIVIDE" if f.args.len() == 2 => {
3850                let mut args = f.args;
3851                let x = args.remove(0);
3852                let y = args.remove(0);
3853                Ok(Expression::Case(Box::new(Case {
3854                    operand: None,
3855                    whens: vec![(
3856                        Expression::Neq(Box::new(BinaryOp {
3857                            left: y.clone(),
3858                            right: Expression::number(0),
3859                            left_comments: Vec::new(),
3860                            operator_comments: Vec::new(),
3861                            trailing_comments: Vec::new(),
3862                            inferred_type: None,
3863                        })),
3864                        Expression::Div(Box::new(BinaryOp {
3865                            left: x,
3866                            right: y,
3867                            left_comments: Vec::new(),
3868                            operator_comments: Vec::new(),
3869                            trailing_comments: Vec::new(),
3870                            inferred_type: None,
3871                        })),
3872                    )],
3873                    else_: Some(Expression::Null(crate::expressions::Null)),
3874                    comments: Vec::new(),
3875                    inferred_type: None,
3876                })))
3877            }
3878
3879            // TO_HEX(x) -> LOWER(HEX(x)) in DuckDB (BigQuery TO_HEX returns lowercase)
3880            "TO_HEX" if f.args.len() == 1 => {
3881                let arg = f.args.into_iter().next().unwrap();
3882                Ok(Expression::Lower(Box::new(UnaryFunc::new(
3883                    Expression::Function(Box::new(Function::new("HEX".to_string(), vec![arg]))),
3884                ))))
3885            }
3886
3887            // EDIT_DISTANCE -> LEVENSHTEIN in DuckDB
3888            "EDIT_DISTANCE" if f.args.len() >= 2 => {
3889                // Only use the first two args (drop max_distance kwarg)
3890                let mut args = f.args;
3891                let a = args.remove(0);
3892                let b = args.remove(0);
3893                Ok(Expression::Function(Box::new(Function::new(
3894                    "LEVENSHTEIN".to_string(),
3895                    vec![a, b],
3896                ))))
3897            }
3898
3899            // UNIX_DATE(d) -> DATE_DIFF('DAY', CAST('1970-01-01' AS DATE), d) in DuckDB
3900            "UNIX_DATE" if f.args.len() == 1 => {
3901                let arg = f.args.into_iter().next().unwrap();
3902                Ok(Expression::Function(Box::new(Function::new(
3903                    "DATE_DIFF".to_string(),
3904                    vec![
3905                        Expression::Literal(Box::new(Literal::String("DAY".to_string()))),
3906                        Expression::Cast(Box::new(Cast {
3907                            this: Expression::Literal(Box::new(Literal::String(
3908                                "1970-01-01".to_string(),
3909                            ))),
3910                            to: DataType::Date,
3911                            trailing_comments: Vec::new(),
3912                            double_colon_syntax: false,
3913                            format: None,
3914                            default: None,
3915                            inferred_type: None,
3916                        })),
3917                        arg,
3918                    ],
3919                ))))
3920            }
3921
3922            // TIMESTAMP(x) -> CAST(x AS TIMESTAMPTZ) in DuckDB
3923            "TIMESTAMP" if f.args.len() == 1 => {
3924                let arg = f.args.into_iter().next().unwrap();
3925                Ok(Expression::Cast(Box::new(Cast {
3926                    this: arg,
3927                    to: DataType::Custom {
3928                        name: "TIMESTAMPTZ".to_string(),
3929                    },
3930                    trailing_comments: Vec::new(),
3931                    double_colon_syntax: false,
3932                    format: None,
3933                    default: None,
3934                    inferred_type: None,
3935                })))
3936            }
3937
3938            // TIME(h, m, s) -> MAKE_TIME(h, m, s) in DuckDB
3939            "TIME" if f.args.len() == 3 => Ok(Expression::Function(Box::new(Function::new(
3940                "MAKE_TIME".to_string(),
3941                f.args,
3942            )))),
3943
3944            // DATE(y, m, d) -> MAKE_DATE(y, m, d) in DuckDB
3945            "DATE" if f.args.len() == 3 => Ok(Expression::Function(Box::new(Function::new(
3946                "MAKE_DATE".to_string(),
3947                f.args,
3948            )))),
3949
3950            // DATETIME(y, m, d, h, min, sec) -> MAKE_TIMESTAMP(y, m, d, h, min, sec) in DuckDB
3951            "DATETIME" if f.args.len() == 6 => Ok(Expression::Function(Box::new(Function::new(
3952                "MAKE_TIMESTAMP".to_string(),
3953                f.args,
3954            )))),
3955
3956            // PARSE_TIMESTAMP(fmt, x) -> STRPTIME(x, fmt) in DuckDB (swap args)
3957            "PARSE_TIMESTAMP" if f.args.len() >= 2 => {
3958                let mut args = f.args;
3959                let fmt = args.remove(0);
3960                let value = args.remove(0);
3961                // Convert BigQuery format to DuckDB strptime format
3962                let duckdb_fmt = self.convert_bq_to_strptime_format(fmt);
3963                Ok(Expression::Function(Box::new(Function::new(
3964                    "STRPTIME".to_string(),
3965                    vec![value, duckdb_fmt],
3966                ))))
3967            }
3968
3969            // BOOLAND(a, b) -> ((ROUND(a, 0)) AND (ROUND(b, 0)))
3970            "BOOLAND" if f.args.len() == 2 => {
3971                let mut args = f.args;
3972                let a = args.remove(0);
3973                let b = args.remove(0);
3974                let ra = Expression::Function(Box::new(Function::new(
3975                    "ROUND".to_string(),
3976                    vec![a, Expression::number(0)],
3977                )));
3978                let rb = Expression::Function(Box::new(Function::new(
3979                    "ROUND".to_string(),
3980                    vec![b, Expression::number(0)],
3981                )));
3982                Ok(Expression::Paren(Box::new(Paren {
3983                    this: Expression::And(Box::new(BinaryOp {
3984                        left: Expression::Paren(Box::new(Paren {
3985                            this: ra,
3986                            trailing_comments: Vec::new(),
3987                        })),
3988                        right: Expression::Paren(Box::new(Paren {
3989                            this: rb,
3990                            trailing_comments: Vec::new(),
3991                        })),
3992                        left_comments: Vec::new(),
3993                        operator_comments: Vec::new(),
3994                        trailing_comments: Vec::new(),
3995                        inferred_type: None,
3996                    })),
3997                    trailing_comments: Vec::new(),
3998                })))
3999            }
4000
4001            // BOOLOR(a, b) -> ((ROUND(a, 0)) OR (ROUND(b, 0)))
4002            "BOOLOR" if f.args.len() == 2 => {
4003                let mut args = f.args;
4004                let a = args.remove(0);
4005                let b = args.remove(0);
4006                let ra = Expression::Function(Box::new(Function::new(
4007                    "ROUND".to_string(),
4008                    vec![a, Expression::number(0)],
4009                )));
4010                let rb = Expression::Function(Box::new(Function::new(
4011                    "ROUND".to_string(),
4012                    vec![b, Expression::number(0)],
4013                )));
4014                Ok(Expression::Paren(Box::new(Paren {
4015                    this: Expression::Or(Box::new(BinaryOp {
4016                        left: Expression::Paren(Box::new(Paren {
4017                            this: ra,
4018                            trailing_comments: Vec::new(),
4019                        })),
4020                        right: Expression::Paren(Box::new(Paren {
4021                            this: rb,
4022                            trailing_comments: Vec::new(),
4023                        })),
4024                        left_comments: Vec::new(),
4025                        operator_comments: Vec::new(),
4026                        trailing_comments: Vec::new(),
4027                        inferred_type: None,
4028                    })),
4029                    trailing_comments: Vec::new(),
4030                })))
4031            }
4032
4033            // BOOLXOR(a, b) -> (ROUND(a, 0) AND (NOT ROUND(b, 0))) OR ((NOT ROUND(a, 0)) AND ROUND(b, 0))
4034            "BOOLXOR" if f.args.len() == 2 => {
4035                let mut args = f.args;
4036                let a = args.remove(0);
4037                let b = args.remove(0);
4038                let ra = Expression::Function(Box::new(Function::new(
4039                    "ROUND".to_string(),
4040                    vec![a, Expression::number(0)],
4041                )));
4042                let rb = Expression::Function(Box::new(Function::new(
4043                    "ROUND".to_string(),
4044                    vec![b, Expression::number(0)],
4045                )));
4046                // (ra AND (NOT rb)) OR ((NOT ra) AND rb)
4047                let not_rb = Expression::Not(Box::new(crate::expressions::UnaryOp {
4048                    this: rb.clone(),
4049                    inferred_type: None,
4050                }));
4051                let not_ra = Expression::Not(Box::new(crate::expressions::UnaryOp {
4052                    this: ra.clone(),
4053                    inferred_type: None,
4054                }));
4055                let left_and = Expression::And(Box::new(BinaryOp {
4056                    left: ra,
4057                    right: Expression::Paren(Box::new(Paren {
4058                        this: not_rb,
4059                        trailing_comments: Vec::new(),
4060                    })),
4061                    left_comments: Vec::new(),
4062                    operator_comments: Vec::new(),
4063                    trailing_comments: Vec::new(),
4064                    inferred_type: None,
4065                }));
4066                let right_and = Expression::And(Box::new(BinaryOp {
4067                    left: Expression::Paren(Box::new(Paren {
4068                        this: not_ra,
4069                        trailing_comments: Vec::new(),
4070                    })),
4071                    right: rb,
4072                    left_comments: Vec::new(),
4073                    operator_comments: Vec::new(),
4074                    trailing_comments: Vec::new(),
4075                    inferred_type: None,
4076                }));
4077                Ok(Expression::Or(Box::new(BinaryOp {
4078                    left: Expression::Paren(Box::new(Paren {
4079                        this: left_and,
4080                        trailing_comments: Vec::new(),
4081                    })),
4082                    right: Expression::Paren(Box::new(Paren {
4083                        this: right_and,
4084                        trailing_comments: Vec::new(),
4085                    })),
4086                    left_comments: Vec::new(),
4087                    operator_comments: Vec::new(),
4088                    trailing_comments: Vec::new(),
4089                    inferred_type: None,
4090                })))
4091            }
4092
4093            // DECODE(expr, search1, result1, ..., default) -> CASE WHEN expr = search1 THEN result1 ... ELSE default END
4094            // For NULL search values, use IS NULL instead of = NULL
4095            "DECODE" if f.args.len() >= 3 => {
4096                let mut args = f.args;
4097                let expr = args.remove(0);
4098                let mut whens = Vec::new();
4099                let mut else_expr = None;
4100                while args.len() >= 2 {
4101                    let search = args.remove(0);
4102                    let result = args.remove(0);
4103                    // For NULL search values, use IS NULL; otherwise use =
4104                    let condition = if matches!(&search, Expression::Null(_)) {
4105                        Expression::IsNull(Box::new(crate::expressions::IsNull {
4106                            this: expr.clone(),
4107                            not: false,
4108                            postfix_form: false,
4109                        }))
4110                    } else {
4111                        Expression::Eq(Box::new(BinaryOp {
4112                            left: expr.clone(),
4113                            right: search,
4114                            left_comments: Vec::new(),
4115                            operator_comments: Vec::new(),
4116                            trailing_comments: Vec::new(),
4117                            inferred_type: None,
4118                        }))
4119                    };
4120                    whens.push((condition, result));
4121                }
4122                if !args.is_empty() {
4123                    else_expr = Some(args.remove(0));
4124                }
4125                Ok(Expression::Case(Box::new(Case {
4126                    operand: None,
4127                    whens,
4128                    else_: else_expr,
4129                    comments: Vec::new(),
4130                    inferred_type: None,
4131                })))
4132            }
4133
4134            // TRY_TO_BOOLEAN -> CASE WHEN UPPER(CAST(x AS TEXT)) = 'ON' THEN TRUE WHEN ... = 'OFF' THEN FALSE ELSE TRY_CAST(x AS BOOLEAN) END
4135            "TRY_TO_BOOLEAN" if f.args.len() == 1 => {
4136                let arg = f.args.into_iter().next().unwrap();
4137                let cast_text = Expression::Cast(Box::new(Cast {
4138                    this: arg.clone(),
4139                    to: DataType::Text,
4140                    trailing_comments: Vec::new(),
4141                    double_colon_syntax: false,
4142                    format: None,
4143                    default: None,
4144                    inferred_type: None,
4145                }));
4146                let upper_text = Expression::Upper(Box::new(UnaryFunc::new(cast_text)));
4147                Ok(Expression::Case(Box::new(Case {
4148                    operand: None,
4149                    whens: vec![
4150                        (
4151                            Expression::Eq(Box::new(BinaryOp {
4152                                left: upper_text.clone(),
4153                                right: Expression::Literal(Box::new(Literal::String(
4154                                    "ON".to_string(),
4155                                ))),
4156                                left_comments: Vec::new(),
4157                                operator_comments: Vec::new(),
4158                                trailing_comments: Vec::new(),
4159                                inferred_type: None,
4160                            })),
4161                            Expression::Boolean(crate::expressions::BooleanLiteral { value: true }),
4162                        ),
4163                        (
4164                            Expression::Eq(Box::new(BinaryOp {
4165                                left: upper_text,
4166                                right: Expression::Literal(Box::new(Literal::String(
4167                                    "OFF".to_string(),
4168                                ))),
4169                                left_comments: Vec::new(),
4170                                operator_comments: Vec::new(),
4171                                trailing_comments: Vec::new(),
4172                                inferred_type: None,
4173                            })),
4174                            Expression::Boolean(crate::expressions::BooleanLiteral {
4175                                value: false,
4176                            }),
4177                        ),
4178                    ],
4179                    else_: Some(Expression::TryCast(Box::new(Cast {
4180                        this: arg,
4181                        to: DataType::Boolean,
4182                        trailing_comments: Vec::new(),
4183                        double_colon_syntax: false,
4184                        format: None,
4185                        default: None,
4186                        inferred_type: None,
4187                    }))),
4188                    comments: Vec::new(),
4189                    inferred_type: None,
4190                })))
4191            }
4192
4193            // TO_BOOLEAN -> complex CASE expression
4194            "TO_BOOLEAN" if f.args.len() == 1 => {
4195                let arg = f.args.into_iter().next().unwrap();
4196                let cast_text = Expression::Cast(Box::new(Cast {
4197                    this: arg.clone(),
4198                    to: DataType::Text,
4199                    trailing_comments: Vec::new(),
4200                    double_colon_syntax: false,
4201                    format: None,
4202                    default: None,
4203                    inferred_type: None,
4204                }));
4205                let upper_text = Expression::Upper(Box::new(UnaryFunc::new(cast_text)));
4206                Ok(Expression::Case(Box::new(Case {
4207                    operand: None,
4208                    whens: vec![
4209                        (
4210                            Expression::Eq(Box::new(BinaryOp {
4211                                left: upper_text.clone(),
4212                                right: Expression::Literal(Box::new(Literal::String(
4213                                    "ON".to_string(),
4214                                ))),
4215                                left_comments: Vec::new(),
4216                                operator_comments: Vec::new(),
4217                                trailing_comments: Vec::new(),
4218                                inferred_type: None,
4219                            })),
4220                            Expression::Boolean(crate::expressions::BooleanLiteral { value: true }),
4221                        ),
4222                        (
4223                            Expression::Eq(Box::new(BinaryOp {
4224                                left: upper_text,
4225                                right: Expression::Literal(Box::new(Literal::String(
4226                                    "OFF".to_string(),
4227                                ))),
4228                                left_comments: Vec::new(),
4229                                operator_comments: Vec::new(),
4230                                trailing_comments: Vec::new(),
4231                                inferred_type: None,
4232                            })),
4233                            Expression::Boolean(crate::expressions::BooleanLiteral {
4234                                value: false,
4235                            }),
4236                        ),
4237                        (
4238                            Expression::Or(Box::new(BinaryOp {
4239                                left: Expression::Function(Box::new(Function::new(
4240                                    "ISNAN".to_string(),
4241                                    vec![Expression::TryCast(Box::new(Cast {
4242                                        this: arg.clone(),
4243                                        to: DataType::Custom {
4244                                            name: "REAL".to_string(),
4245                                        },
4246                                        trailing_comments: Vec::new(),
4247                                        double_colon_syntax: false,
4248                                        format: None,
4249                                        default: None,
4250                                        inferred_type: None,
4251                                    }))],
4252                                ))),
4253                                right: Expression::Function(Box::new(Function::new(
4254                                    "ISINF".to_string(),
4255                                    vec![Expression::TryCast(Box::new(Cast {
4256                                        this: arg.clone(),
4257                                        to: DataType::Custom {
4258                                            name: "REAL".to_string(),
4259                                        },
4260                                        trailing_comments: Vec::new(),
4261                                        double_colon_syntax: false,
4262                                        format: None,
4263                                        default: None,
4264                                        inferred_type: None,
4265                                    }))],
4266                                ))),
4267                                left_comments: Vec::new(),
4268                                operator_comments: Vec::new(),
4269                                trailing_comments: Vec::new(),
4270                                inferred_type: None,
4271                            })),
4272                            Expression::Function(Box::new(Function::new(
4273                                "ERROR".to_string(),
4274                                vec![Expression::Literal(Box::new(Literal::String(
4275                                    "TO_BOOLEAN: Non-numeric values NaN and INF are not supported"
4276                                        .to_string(),
4277                                )))],
4278                            ))),
4279                        ),
4280                    ],
4281                    else_: Some(Expression::Cast(Box::new(Cast {
4282                        this: arg,
4283                        to: DataType::Boolean,
4284                        trailing_comments: Vec::new(),
4285                        double_colon_syntax: false,
4286                        format: None,
4287                        default: None,
4288                        inferred_type: None,
4289                    }))),
4290                    comments: Vec::new(),
4291                    inferred_type: None,
4292                })))
4293            }
4294
4295            // OBJECT_INSERT(obj, key, value) -> STRUCT_INSERT(obj, key := value)
4296            // Special case: OBJECT_INSERT(OBJECT_CONSTRUCT(), key, value) -> STRUCT_PACK(key := value)
4297            "OBJECT_INSERT" if f.args.len() == 3 => {
4298                let mut args = f.args;
4299                let obj = args.remove(0);
4300                let key = args.remove(0);
4301                let value = args.remove(0);
4302                // Extract key string for named arg
4303                let key_name = match &key {
4304                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
4305                        let Literal::String(s) = lit.as_ref() else {
4306                            unreachable!()
4307                        };
4308                        s.clone()
4309                    }
4310                    _ => "key".to_string(),
4311                };
4312                let named_arg =
4313                    Expression::NamedArgument(Box::new(crate::expressions::NamedArgument {
4314                        name: Identifier::new(&key_name),
4315                        value,
4316                        separator: crate::expressions::NamedArgSeparator::ColonEq,
4317                    }));
4318                // Check if the inner object is an empty STRUCT_PACK or OBJECT_CONSTRUCT
4319                let is_empty_struct = match &obj {
4320                    Expression::Struct(s) if s.fields.is_empty() => true,
4321                    Expression::Function(f) => {
4322                        let n = f.name.to_uppercase();
4323                        (n == "STRUCT_PACK" || n == "OBJECT_CONSTRUCT") && f.args.is_empty()
4324                    }
4325                    _ => false,
4326                };
4327                if is_empty_struct {
4328                    // Collapse: OBJECT_INSERT(empty, key, value) -> STRUCT_PACK(key := value)
4329                    Ok(Expression::Function(Box::new(Function::new(
4330                        "STRUCT_PACK".to_string(),
4331                        vec![named_arg],
4332                    ))))
4333                } else {
4334                    Ok(Expression::Function(Box::new(Function::new(
4335                        "STRUCT_INSERT".to_string(),
4336                        vec![obj, named_arg],
4337                    ))))
4338                }
4339            }
4340
4341            // GET(array_or_obj, key) -> array[key+1] for arrays, obj -> '$.key' for objects
4342            "GET" if f.args.len() == 2 => {
4343                let mut args = f.args;
4344                let this = args.remove(0);
4345                let key = args.remove(0);
4346                match &key {
4347                    // String key -> JSON extract (object access)
4348                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
4349                        let Literal::String(s) = lit.as_ref() else {
4350                            unreachable!()
4351                        };
4352                        let json_path = format!("$.{}", s);
4353                        Ok(Expression::JsonExtract(Box::new(JsonExtractFunc {
4354                            options: None,
4355                            source_dialect: None,
4356
4357                            this,
4358                            path: Expression::Literal(Box::new(Literal::String(json_path))),
4359                            returning: None,
4360                            arrow_syntax: true,
4361                            hash_arrow_syntax: false,
4362                            wrapper_option: None,
4363                            quotes_option: None,
4364                            on_scalar_string: false,
4365                            on_error: None,
4366                        })))
4367                    }
4368                    // Numeric key -> array subscript
4369                    // For MAP access: key is used as-is (map[key])
4370                    // For ARRAY access: Snowflake is 0-based, DuckDB is 1-based, so add 1
4371                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => {
4372                        let Literal::Number(n) = lit.as_ref() else {
4373                            unreachable!()
4374                        };
4375                        let idx: i64 = n.parse().unwrap_or(0);
4376                        let is_map = matches!(&this, Expression::Cast(c) if matches!(c.to, DataType::Map { .. }));
4377                        let index_val = if is_map { idx } else { idx + 1 };
4378                        Ok(Expression::Subscript(Box::new(
4379                            crate::expressions::Subscript {
4380                                this,
4381                                index: Expression::number(index_val),
4382                            },
4383                        )))
4384                    }
4385                    _ => {
4386                        // Unknown key type - use JSON arrow
4387                        Ok(Expression::JsonExtract(Box::new(JsonExtractFunc {
4388                            options: None,
4389                            source_dialect: None,
4390
4391                            this,
4392                            path: Expression::JSONPath(Box::new(JSONPath {
4393                                expressions: vec![
4394                                    Expression::JSONPathRoot(JSONPathRoot),
4395                                    Expression::JSONPathKey(Box::new(JSONPathKey {
4396                                        this: Box::new(key),
4397                                    })),
4398                                ],
4399                                escape: None,
4400                            })),
4401                            returning: None,
4402                            arrow_syntax: true,
4403                            hash_arrow_syntax: false,
4404                            wrapper_option: None,
4405                            quotes_option: None,
4406                            on_scalar_string: false,
4407                            on_error: None,
4408                        })))
4409                    }
4410                }
4411            }
4412
4413            // GET_PATH(obj, path) -> obj -> json_path in DuckDB
4414            "GET_PATH" if f.args.len() == 2 => {
4415                let mut args = f.args;
4416                let this = args.remove(0);
4417                let path = args.remove(0);
4418                // Convert Snowflake path to JSONPath
4419                let json_path = match &path {
4420                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
4421                        let Literal::String(s) = lit.as_ref() else {
4422                            unreachable!()
4423                        };
4424                        // Convert bracket notation ["key"] to quoted dot notation ."key"
4425                        let s = Self::convert_bracket_to_quoted_path(s);
4426                        // Convert Snowflake path (e.g., 'attr[0].name' or '[0].attr') to JSON path ($.attr[0].name or $[0].attr)
4427                        let normalized = if s.starts_with('$') {
4428                            s
4429                        } else if s.starts_with('[') {
4430                            format!("${}", s)
4431                        } else {
4432                            format!("$.{}", s)
4433                        };
4434                        Expression::Literal(Box::new(Literal::String(normalized)))
4435                    }
4436                    _ => path,
4437                };
4438                Ok(Expression::JsonExtract(Box::new(JsonExtractFunc {
4439                    options: None,
4440                    source_dialect: None,
4441
4442                    this,
4443                    path: json_path,
4444                    returning: None,
4445                    arrow_syntax: true,
4446                    hash_arrow_syntax: false,
4447                    wrapper_option: None,
4448                    quotes_option: None,
4449                    on_scalar_string: false,
4450                    on_error: None,
4451                })))
4452            }
4453
4454            // BASE64_ENCODE(x) -> TO_BASE64(x)
4455            "BASE64_ENCODE" if f.args.len() == 1 => Ok(Expression::Function(Box::new(
4456                Function::new("TO_BASE64".to_string(), f.args),
4457            ))),
4458
4459            // BASE64_ENCODE(x, max_line_length) -> RTRIM(REGEXP_REPLACE(TO_BASE64(x), '(.{N})', '\1' || CHR(10), 'g'), CHR(10))
4460            "BASE64_ENCODE" if f.args.len() >= 2 => {
4461                let mut args = f.args;
4462                let x = args.remove(0);
4463                let line_len = args.remove(0);
4464                let line_len_str = match &line_len {
4465                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => {
4466                        let Literal::Number(n) = lit.as_ref() else {
4467                            unreachable!()
4468                        };
4469                        n.clone()
4470                    }
4471                    _ => "76".to_string(),
4472                };
4473                let to_base64 =
4474                    Expression::Function(Box::new(Function::new("TO_BASE64".to_string(), vec![x])));
4475                let pattern = format!("(.{{{}}})", line_len_str);
4476                let chr_10 = Expression::Function(Box::new(Function::new(
4477                    "CHR".to_string(),
4478                    vec![Expression::number(10)],
4479                )));
4480                let replacement = Expression::Concat(Box::new(BinaryOp {
4481                    left: Expression::Literal(Box::new(Literal::String("\\1".to_string()))),
4482                    right: chr_10.clone(),
4483                    left_comments: Vec::new(),
4484                    operator_comments: Vec::new(),
4485                    trailing_comments: Vec::new(),
4486                    inferred_type: None,
4487                }));
4488                let regexp_replace = Expression::Function(Box::new(Function::new(
4489                    "REGEXP_REPLACE".to_string(),
4490                    vec![
4491                        to_base64,
4492                        Expression::Literal(Box::new(Literal::String(pattern))),
4493                        replacement,
4494                        Expression::Literal(Box::new(Literal::String("g".to_string()))),
4495                    ],
4496                )));
4497                Ok(Expression::Function(Box::new(Function::new(
4498                    "RTRIM".to_string(),
4499                    vec![regexp_replace, chr_10],
4500                ))))
4501            }
4502
4503            // TRY_TO_DATE with 2 args -> CAST(CAST(TRY_STRPTIME(value, fmt) AS TIMESTAMP) AS DATE)
4504            "TRY_TO_DATE" if f.args.len() == 2 => {
4505                let mut args = f.args;
4506                let value = args.remove(0);
4507                let fmt = self.convert_snowflake_date_format(args.remove(0));
4508                Ok(Expression::Cast(Box::new(Cast {
4509                    this: Expression::Cast(Box::new(Cast {
4510                        this: Expression::Function(Box::new(Function::new(
4511                            "TRY_STRPTIME".to_string(),
4512                            vec![value, fmt],
4513                        ))),
4514                        to: DataType::Timestamp {
4515                            precision: None,
4516                            timezone: false,
4517                        },
4518                        trailing_comments: Vec::new(),
4519                        double_colon_syntax: false,
4520                        format: None,
4521                        default: None,
4522                        inferred_type: None,
4523                    })),
4524                    to: DataType::Date,
4525                    trailing_comments: Vec::new(),
4526                    double_colon_syntax: false,
4527                    format: None,
4528                    default: None,
4529                    inferred_type: None,
4530                })))
4531            }
4532
4533            // REGEXP_REPLACE with 4 args: check if 4th arg is a number (Snowflake position) or flags (DuckDB native)
4534            // REGEXP_REPLACE with 4 args: check if 4th is a string flag (DuckDB native) or a numeric position
4535            "REGEXP_REPLACE" if f.args.len() == 4 => {
4536                let is_snowflake_position = matches!(&f.args[3], Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)));
4537                if is_snowflake_position {
4538                    // Snowflake form: REGEXP_REPLACE(subject, pattern, replacement, position) -> add 'g' flag
4539                    let mut args = f.args;
4540                    let subject = args.remove(0);
4541                    let pattern = args.remove(0);
4542                    let replacement = args.remove(0);
4543                    Ok(Expression::Function(Box::new(Function::new(
4544                        "REGEXP_REPLACE".to_string(),
4545                        vec![
4546                            subject,
4547                            pattern,
4548                            replacement,
4549                            Expression::Literal(Box::new(Literal::String("g".to_string()))),
4550                        ],
4551                    ))))
4552                } else {
4553                    // DuckDB native form (string flags) or pass through
4554                    Ok(Expression::Function(Box::new(f)))
4555                }
4556            }
4557
4558            // REGEXP_REPLACE with 5+ args -> Snowflake form: (subject, pattern, replacement, position, occurrence, params)
4559            "REGEXP_REPLACE" if f.args.len() >= 5 => {
4560                let mut args = f.args;
4561                let subject = args.remove(0);
4562                let pattern = args.remove(0);
4563                let replacement = args.remove(0);
4564                let _position = args.remove(0);
4565                let occurrence = if !args.is_empty() {
4566                    Some(args.remove(0))
4567                } else {
4568                    None
4569                };
4570                let params = if !args.is_empty() {
4571                    Some(args.remove(0))
4572                } else {
4573                    None
4574                };
4575
4576                let mut flags = String::new();
4577                if let Some(Expression::Literal(lit)) = &params {
4578                    if let Literal::String(p) = lit.as_ref() {
4579                        flags = p.clone();
4580                    }
4581                }
4582                let is_global = match &occurrence {
4583                    Some(Expression::Literal(lit))
4584                        if matches!(lit.as_ref(), Literal::Number(_)) =>
4585                    {
4586                        let Literal::Number(n) = lit.as_ref() else {
4587                            unreachable!()
4588                        };
4589                        n == "0"
4590                    }
4591                    None => true,
4592                    _ => false,
4593                };
4594                if is_global && !flags.contains('g') {
4595                    flags.push('g');
4596                }
4597
4598                Ok(Expression::Function(Box::new(Function::new(
4599                    "REGEXP_REPLACE".to_string(),
4600                    vec![
4601                        subject,
4602                        pattern,
4603                        replacement,
4604                        Expression::Literal(Box::new(Literal::String(flags))),
4605                    ],
4606                ))))
4607            }
4608
4609            // ROUND with named args (EXPR =>, SCALE =>, ROUNDING_MODE =>)
4610            "ROUND"
4611                if f.args
4612                    .iter()
4613                    .any(|a| matches!(a, Expression::NamedArgument(_))) =>
4614            {
4615                let mut expr_val = None;
4616                let mut scale_val = None;
4617                let mut rounding_mode = None;
4618                for arg in &f.args {
4619                    if let Expression::NamedArgument(na) = arg {
4620                        match na.name.name.to_uppercase().as_str() {
4621                            "EXPR" => expr_val = Some(na.value.clone()),
4622                            "SCALE" => scale_val = Some(na.value.clone()),
4623                            "ROUNDING_MODE" => rounding_mode = Some(na.value.clone()),
4624                            _ => {}
4625                        }
4626                    }
4627                }
4628                if let Some(expr) = expr_val {
4629                    let scale = scale_val.unwrap_or(Expression::number(0));
4630                    let is_half_to_even = match &rounding_mode {
4631                        Some(Expression::Literal(lit))
4632                            if matches!(lit.as_ref(), Literal::String(_)) =>
4633                        {
4634                            let Literal::String(s) = lit.as_ref() else {
4635                                unreachable!()
4636                            };
4637                            s == "HALF_TO_EVEN"
4638                        }
4639                        _ => false,
4640                    };
4641                    if is_half_to_even {
4642                        Ok(Expression::Function(Box::new(Function::new(
4643                            "ROUND_EVEN".to_string(),
4644                            vec![expr, scale],
4645                        ))))
4646                    } else {
4647                        Ok(Expression::Function(Box::new(Function::new(
4648                            "ROUND".to_string(),
4649                            vec![expr, scale],
4650                        ))))
4651                    }
4652                } else {
4653                    Ok(Expression::Function(Box::new(f)))
4654                }
4655            }
4656
4657            // ROUND(x, scale, 'HALF_TO_EVEN') -> ROUND_EVEN(x, scale)
4658            // ROUND(x, scale, 'HALF_AWAY_FROM_ZERO') -> ROUND(x, scale)
4659            "ROUND" if f.args.len() == 3 => {
4660                let mut args = f.args;
4661                let x = args.remove(0);
4662                let scale = args.remove(0);
4663                let mode = args.remove(0);
4664                let is_half_to_even = match &mode {
4665                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
4666                        let Literal::String(s) = lit.as_ref() else {
4667                            unreachable!()
4668                        };
4669                        s == "HALF_TO_EVEN"
4670                    }
4671                    _ => false,
4672                };
4673                if is_half_to_even {
4674                    Ok(Expression::Function(Box::new(Function::new(
4675                        "ROUND_EVEN".to_string(),
4676                        vec![x, scale],
4677                    ))))
4678                } else {
4679                    // HALF_AWAY_FROM_ZERO is default in DuckDB, just drop the mode
4680                    Ok(Expression::Function(Box::new(Function::new(
4681                        "ROUND".to_string(),
4682                        vec![x, scale],
4683                    ))))
4684                }
4685            }
4686
4687            // ROUND(x, scale) where scale is non-integer -> ROUND(x, CAST(scale AS INT))
4688            "ROUND" if f.args.len() == 2 => {
4689                let mut args = f.args;
4690                let x = args.remove(0);
4691                let scale = args.remove(0);
4692                let needs_cast = match &scale {
4693                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => {
4694                        let Literal::Number(n) = lit.as_ref() else {
4695                            unreachable!()
4696                        };
4697                        n.contains('.')
4698                    }
4699                    Expression::Cast(_) => {
4700                        // Already has a CAST - wrap in another CAST to INT
4701                        true
4702                    }
4703                    _ => false,
4704                };
4705                if needs_cast {
4706                    Ok(Expression::Function(Box::new(Function::new(
4707                        "ROUND".to_string(),
4708                        vec![
4709                            x,
4710                            Expression::Cast(Box::new(Cast {
4711                                this: scale,
4712                                to: DataType::Int {
4713                                    length: None,
4714                                    integer_spelling: false,
4715                                },
4716                                trailing_comments: Vec::new(),
4717                                double_colon_syntax: false,
4718                                format: None,
4719                                default: None,
4720                                inferred_type: None,
4721                            })),
4722                        ],
4723                    ))))
4724                } else {
4725                    Ok(Expression::Function(Box::new(Function::new(
4726                        "ROUND".to_string(),
4727                        vec![x, scale],
4728                    ))))
4729                }
4730            }
4731
4732            // FLOOR(x, scale) -> ROUND(FLOOR(x * POWER(10, scale)) / POWER(10, scale), scale)
4733            "FLOOR" if f.args.len() == 2 => {
4734                let mut args = f.args;
4735                let x = args.remove(0);
4736                let scale = args.remove(0);
4737                // Check if scale needs CAST to INT
4738                let needs_cast = match &scale {
4739                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => {
4740                        let Literal::Number(n) = lit.as_ref() else {
4741                            unreachable!()
4742                        };
4743                        n.contains('.')
4744                    }
4745                    _ => false,
4746                };
4747                let int_scale = if needs_cast {
4748                    Expression::Cast(Box::new(Cast {
4749                        this: scale.clone(),
4750                        to: DataType::Int {
4751                            length: None,
4752                            integer_spelling: false,
4753                        },
4754                        trailing_comments: Vec::new(),
4755                        double_colon_syntax: false,
4756                        format: None,
4757                        default: None,
4758                        inferred_type: None,
4759                    }))
4760                } else {
4761                    scale.clone()
4762                };
4763                let power_10 = Expression::Function(Box::new(Function::new(
4764                    "POWER".to_string(),
4765                    vec![Expression::number(10), int_scale.clone()],
4766                )));
4767                let x_paren = match &x {
4768                    Expression::Add(_)
4769                    | Expression::Sub(_)
4770                    | Expression::Mul(_)
4771                    | Expression::Div(_) => Expression::Paren(Box::new(Paren {
4772                        this: x,
4773                        trailing_comments: Vec::new(),
4774                    })),
4775                    _ => x,
4776                };
4777                let multiplied = Expression::Mul(Box::new(BinaryOp {
4778                    left: x_paren,
4779                    right: power_10.clone(),
4780                    left_comments: Vec::new(),
4781                    operator_comments: Vec::new(),
4782                    trailing_comments: Vec::new(),
4783                    inferred_type: None,
4784                }));
4785                let floored = Expression::Function(Box::new(Function::new(
4786                    "FLOOR".to_string(),
4787                    vec![multiplied],
4788                )));
4789                let divided = Expression::Div(Box::new(BinaryOp {
4790                    left: floored,
4791                    right: power_10,
4792                    left_comments: Vec::new(),
4793                    operator_comments: Vec::new(),
4794                    trailing_comments: Vec::new(),
4795                    inferred_type: None,
4796                }));
4797                Ok(Expression::Function(Box::new(Function::new(
4798                    "ROUND".to_string(),
4799                    vec![divided, int_scale],
4800                ))))
4801            }
4802
4803            // CEIL(x, scale) -> ROUND(CEIL(x * POWER(10, scale)) / POWER(10, scale), scale)
4804            "CEIL" | "CEILING" if f.args.len() == 2 => {
4805                let mut args = f.args;
4806                let x = args.remove(0);
4807                let scale = args.remove(0);
4808                let needs_cast = match &scale {
4809                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => {
4810                        let Literal::Number(n) = lit.as_ref() else {
4811                            unreachable!()
4812                        };
4813                        n.contains('.')
4814                    }
4815                    _ => false,
4816                };
4817                let int_scale = if needs_cast {
4818                    Expression::Cast(Box::new(Cast {
4819                        this: scale.clone(),
4820                        to: DataType::Int {
4821                            length: None,
4822                            integer_spelling: false,
4823                        },
4824                        trailing_comments: Vec::new(),
4825                        double_colon_syntax: false,
4826                        format: None,
4827                        default: None,
4828                        inferred_type: None,
4829                    }))
4830                } else {
4831                    scale.clone()
4832                };
4833                let power_10 = Expression::Function(Box::new(Function::new(
4834                    "POWER".to_string(),
4835                    vec![Expression::number(10), int_scale.clone()],
4836                )));
4837                let x_paren = match &x {
4838                    Expression::Add(_)
4839                    | Expression::Sub(_)
4840                    | Expression::Mul(_)
4841                    | Expression::Div(_) => Expression::Paren(Box::new(Paren {
4842                        this: x,
4843                        trailing_comments: Vec::new(),
4844                    })),
4845                    _ => x,
4846                };
4847                let multiplied = Expression::Mul(Box::new(BinaryOp {
4848                    left: x_paren,
4849                    right: power_10.clone(),
4850                    left_comments: Vec::new(),
4851                    operator_comments: Vec::new(),
4852                    trailing_comments: Vec::new(),
4853                    inferred_type: None,
4854                }));
4855                let ceiled = Expression::Function(Box::new(Function::new(
4856                    "CEIL".to_string(),
4857                    vec![multiplied],
4858                )));
4859                let divided = Expression::Div(Box::new(BinaryOp {
4860                    left: ceiled,
4861                    right: power_10,
4862                    left_comments: Vec::new(),
4863                    operator_comments: Vec::new(),
4864                    trailing_comments: Vec::new(),
4865                    inferred_type: None,
4866                }));
4867                Ok(Expression::Function(Box::new(Function::new(
4868                    "ROUND".to_string(),
4869                    vec![divided, int_scale],
4870                ))))
4871            }
4872
4873            // ADD_MONTHS(date, n) -> CASE WHEN LAST_DAY(date) = date THEN LAST_DAY(date + INTERVAL n MONTH) ELSE date + INTERVAL n MONTH END
4874            "ADD_MONTHS" if f.args.len() == 2 => {
4875                let mut args = f.args;
4876                let date_expr_raw = args.remove(0);
4877                let months_expr = args.remove(0);
4878
4879                // Track whether the raw expression was a string literal
4880                let was_string_literal = matches!(&date_expr_raw, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)));
4881
4882                // Wrap string literals in CAST(... AS TIMESTAMP) for DuckDB
4883                let date_expr = match &date_expr_raw {
4884                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
4885                        Expression::Cast(Box::new(Cast {
4886                            this: date_expr_raw,
4887                            to: DataType::Timestamp {
4888                                precision: None,
4889                                timezone: false,
4890                            },
4891                            trailing_comments: Vec::new(),
4892                            double_colon_syntax: false,
4893                            format: None,
4894                            default: None,
4895                            inferred_type: None,
4896                        }))
4897                    }
4898                    _ => date_expr_raw,
4899                };
4900
4901                // Determine the type of the date expression for outer CAST
4902                // But NOT if the CAST was added by us (for string literal wrapping)
4903                let date_type = if was_string_literal {
4904                    None
4905                } else {
4906                    match &date_expr {
4907                        Expression::Cast(c) => Some(c.to.clone()),
4908                        _ => None,
4909                    }
4910                };
4911
4912                // Determine interval expression - for non-integer months, use TO_MONTHS(CAST(ROUND(n) AS INT))
4913                let is_non_integer_months = match &months_expr {
4914                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => {
4915                        let Literal::Number(n) = lit.as_ref() else {
4916                            unreachable!()
4917                        };
4918                        n.contains('.')
4919                    }
4920                    Expression::Neg(_) => {
4921                        if let Expression::Neg(um) = &months_expr {
4922                            matches!(&um.this, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(n) if n.contains('.')))
4923                        } else {
4924                            false
4925                        }
4926                    }
4927                    // Cast to DECIMAL type means non-integer months
4928                    Expression::Cast(c) => matches!(&c.to, DataType::Decimal { .. }),
4929                    _ => false,
4930                };
4931
4932                let is_negative = match &months_expr {
4933                    Expression::Neg(_) => true,
4934                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => {
4935                        let Literal::Number(n) = lit.as_ref() else {
4936                            unreachable!()
4937                        };
4938                        n.starts_with('-')
4939                    }
4940                    _ => false,
4941                };
4942                let is_null = matches!(&months_expr, Expression::Null(_));
4943
4944                let interval_expr = if is_non_integer_months {
4945                    // For non-integer: TO_MONTHS(CAST(ROUND(n) AS INT))
4946                    Expression::Function(Box::new(Function::new(
4947                        "TO_MONTHS".to_string(),
4948                        vec![Expression::Cast(Box::new(Cast {
4949                            this: Expression::Function(Box::new(Function::new(
4950                                "ROUND".to_string(),
4951                                vec![months_expr.clone()],
4952                            ))),
4953                            to: DataType::Int {
4954                                length: None,
4955                                integer_spelling: false,
4956                            },
4957                            trailing_comments: Vec::new(),
4958                            double_colon_syntax: false,
4959                            format: None,
4960                            default: None,
4961                            inferred_type: None,
4962                        }))],
4963                    )))
4964                } else if is_negative || is_null {
4965                    // For negative or NULL: INTERVAL (n) MONTH
4966                    Expression::Interval(Box::new(Interval {
4967                        this: Some(Expression::Paren(Box::new(Paren {
4968                            this: months_expr.clone(),
4969                            trailing_comments: Vec::new(),
4970                        }))),
4971                        unit: Some(IntervalUnitSpec::Simple {
4972                            unit: IntervalUnit::Month,
4973                            use_plural: false,
4974                        }),
4975                    }))
4976                } else {
4977                    // For positive integer: INTERVAL n MONTH
4978                    Expression::Interval(Box::new(Interval {
4979                        this: Some(months_expr.clone()),
4980                        unit: Some(IntervalUnitSpec::Simple {
4981                            unit: IntervalUnit::Month,
4982                            use_plural: false,
4983                        }),
4984                    }))
4985                };
4986
4987                let date_plus_interval = Expression::Add(Box::new(BinaryOp {
4988                    left: date_expr.clone(),
4989                    right: interval_expr.clone(),
4990                    left_comments: Vec::new(),
4991                    operator_comments: Vec::new(),
4992                    trailing_comments: Vec::new(),
4993                    inferred_type: None,
4994                }));
4995
4996                let case_expr = Expression::Case(Box::new(Case {
4997                    operand: None,
4998                    whens: vec![(
4999                        Expression::Eq(Box::new(BinaryOp {
5000                            left: Expression::Function(Box::new(Function::new(
5001                                "LAST_DAY".to_string(),
5002                                vec![date_expr.clone()],
5003                            ))),
5004                            right: date_expr.clone(),
5005                            left_comments: Vec::new(),
5006                            operator_comments: Vec::new(),
5007                            trailing_comments: Vec::new(),
5008                            inferred_type: None,
5009                        })),
5010                        Expression::Function(Box::new(Function::new(
5011                            "LAST_DAY".to_string(),
5012                            vec![date_plus_interval.clone()],
5013                        ))),
5014                    )],
5015                    else_: Some(date_plus_interval),
5016                    comments: Vec::new(),
5017                    inferred_type: None,
5018                }));
5019
5020                // Wrap in CAST if date had explicit type
5021                if let Some(dt) = date_type {
5022                    Ok(Expression::Cast(Box::new(Cast {
5023                        this: case_expr,
5024                        to: dt,
5025                        trailing_comments: Vec::new(),
5026                        double_colon_syntax: false,
5027                        format: None,
5028                        default: None,
5029                        inferred_type: None,
5030                    })))
5031                } else {
5032                    Ok(case_expr)
5033                }
5034            }
5035
5036            // TIME_SLICE(date, n, 'UNIT') -> TIME_BUCKET(INTERVAL n UNIT, date)
5037            // TIME_SLICE(date, n, 'UNIT', 'END') -> TIME_BUCKET(INTERVAL n UNIT, date) + INTERVAL n UNIT
5038            "TIME_SLICE" if f.args.len() >= 3 => {
5039                let mut args = f.args;
5040                let date_expr = args.remove(0);
5041                let n = args.remove(0);
5042                let unit_str = args.remove(0);
5043                let alignment = if !args.is_empty() {
5044                    Some(args.remove(0))
5045                } else {
5046                    None
5047                };
5048
5049                // Extract unit string
5050                let unit = match &unit_str {
5051                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
5052                        let Literal::String(s) = lit.as_ref() else {
5053                            unreachable!()
5054                        };
5055                        s.to_uppercase()
5056                    }
5057                    Expression::Column(c) => c.name.name.to_uppercase(),
5058                    Expression::Identifier(i) => i.name.to_uppercase(),
5059                    _ => "DAY".to_string(),
5060                };
5061
5062                let interval_unit = match unit.as_str() {
5063                    "YEAR" => IntervalUnit::Year,
5064                    "QUARTER" => IntervalUnit::Quarter,
5065                    "MONTH" => IntervalUnit::Month,
5066                    "WEEK" => IntervalUnit::Week,
5067                    "DAY" => IntervalUnit::Day,
5068                    "HOUR" => IntervalUnit::Hour,
5069                    "MINUTE" => IntervalUnit::Minute,
5070                    "SECOND" => IntervalUnit::Second,
5071                    _ => IntervalUnit::Day,
5072                };
5073
5074                let interval = Expression::Interval(Box::new(Interval {
5075                    this: Some(n.clone()),
5076                    unit: Some(IntervalUnitSpec::Simple {
5077                        unit: interval_unit.clone(),
5078                        use_plural: false,
5079                    }),
5080                }));
5081
5082                let time_bucket = Expression::Function(Box::new(Function::new(
5083                    "TIME_BUCKET".to_string(),
5084                    vec![interval.clone(), date_expr.clone()],
5085                )));
5086
5087                let is_end = match &alignment {
5088                    Some(Expression::Literal(lit))
5089                        if matches!(lit.as_ref(), Literal::String(_)) =>
5090                    {
5091                        let Literal::String(s) = lit.as_ref() else {
5092                            unreachable!()
5093                        };
5094                        s.to_uppercase() == "END"
5095                    }
5096                    _ => false,
5097                };
5098
5099                // Determine if date is a DATE type (needs CAST)
5100                let is_date_type = match &date_expr {
5101                    Expression::Cast(c) => matches!(&c.to, DataType::Date),
5102                    _ => false,
5103                };
5104
5105                if is_end {
5106                    let bucket_plus = Expression::Add(Box::new(BinaryOp {
5107                        left: time_bucket,
5108                        right: Expression::Interval(Box::new(Interval {
5109                            this: Some(n),
5110                            unit: Some(IntervalUnitSpec::Simple {
5111                                unit: interval_unit,
5112                                use_plural: false,
5113                            }),
5114                        })),
5115                        left_comments: Vec::new(),
5116                        operator_comments: Vec::new(),
5117                        trailing_comments: Vec::new(),
5118                        inferred_type: None,
5119                    }));
5120                    if is_date_type {
5121                        Ok(Expression::Cast(Box::new(Cast {
5122                            this: bucket_plus,
5123                            to: DataType::Date,
5124                            trailing_comments: Vec::new(),
5125                            double_colon_syntax: false,
5126                            format: None,
5127                            default: None,
5128                            inferred_type: None,
5129                        })))
5130                    } else {
5131                        Ok(bucket_plus)
5132                    }
5133                } else {
5134                    Ok(time_bucket)
5135                }
5136            }
5137
5138            // DATE_FROM_PARTS(year, month, day) -> CAST(MAKE_DATE(year, 1, 1) + INTERVAL (month - 1) MONTH + INTERVAL (day - 1) DAY AS DATE)
5139            "DATE_FROM_PARTS" | "DATEFROMPARTS" if f.args.len() == 3 => {
5140                let mut args = f.args;
5141                let year = args.remove(0);
5142                let month = args.remove(0);
5143                let day = args.remove(0);
5144
5145                let make_date = Expression::Function(Box::new(Function::new(
5146                    "MAKE_DATE".to_string(),
5147                    vec![year, Expression::number(1), Expression::number(1)],
5148                )));
5149
5150                // Wrap compound expressions in parens to get ((expr) - 1) instead of (expr - 1)
5151                let month_wrapped = match &month {
5152                    Expression::Add(_)
5153                    | Expression::Sub(_)
5154                    | Expression::Mul(_)
5155                    | Expression::Div(_) => Expression::Paren(Box::new(Paren {
5156                        this: month,
5157                        trailing_comments: Vec::new(),
5158                    })),
5159                    _ => month,
5160                };
5161                let day_wrapped = match &day {
5162                    Expression::Add(_)
5163                    | Expression::Sub(_)
5164                    | Expression::Mul(_)
5165                    | Expression::Div(_) => Expression::Paren(Box::new(Paren {
5166                        this: day,
5167                        trailing_comments: Vec::new(),
5168                    })),
5169                    _ => day,
5170                };
5171                let month_minus_1 = Expression::Sub(Box::new(BinaryOp {
5172                    left: month_wrapped,
5173                    right: Expression::number(1),
5174                    left_comments: Vec::new(),
5175                    operator_comments: Vec::new(),
5176                    trailing_comments: Vec::new(),
5177                    inferred_type: None,
5178                }));
5179                let month_interval = Expression::Interval(Box::new(Interval {
5180                    this: Some(Expression::Paren(Box::new(Paren {
5181                        this: month_minus_1,
5182                        trailing_comments: Vec::new(),
5183                    }))),
5184                    unit: Some(IntervalUnitSpec::Simple {
5185                        unit: IntervalUnit::Month,
5186                        use_plural: false,
5187                    }),
5188                }));
5189
5190                let day_minus_1 = Expression::Sub(Box::new(BinaryOp {
5191                    left: day_wrapped,
5192                    right: Expression::number(1),
5193                    left_comments: Vec::new(),
5194                    operator_comments: Vec::new(),
5195                    trailing_comments: Vec::new(),
5196                    inferred_type: None,
5197                }));
5198                let day_interval = Expression::Interval(Box::new(Interval {
5199                    this: Some(Expression::Paren(Box::new(Paren {
5200                        this: day_minus_1,
5201                        trailing_comments: Vec::new(),
5202                    }))),
5203                    unit: Some(IntervalUnitSpec::Simple {
5204                        unit: IntervalUnit::Day,
5205                        use_plural: false,
5206                    }),
5207                }));
5208
5209                let result = Expression::Add(Box::new(BinaryOp {
5210                    left: Expression::Add(Box::new(BinaryOp {
5211                        left: make_date,
5212                        right: month_interval,
5213                        left_comments: Vec::new(),
5214                        operator_comments: Vec::new(),
5215                        trailing_comments: Vec::new(),
5216                        inferred_type: None,
5217                    })),
5218                    right: day_interval,
5219                    left_comments: Vec::new(),
5220                    operator_comments: Vec::new(),
5221                    trailing_comments: Vec::new(),
5222                    inferred_type: None,
5223                }));
5224
5225                Ok(Expression::Cast(Box::new(Cast {
5226                    this: result,
5227                    to: DataType::Date,
5228                    trailing_comments: Vec::new(),
5229                    double_colon_syntax: false,
5230                    format: None,
5231                    default: None,
5232                    inferred_type: None,
5233                })))
5234            }
5235
5236            // NEXT_DAY(date, 'day_name') -> complex expression using ISODOW
5237            "NEXT_DAY" if f.args.len() == 2 => {
5238                let mut args = f.args;
5239                let date = args.remove(0);
5240                let day_name = args.remove(0);
5241
5242                // Parse day name to ISO day number (1=Monday..7=Sunday)
5243                let day_num = match &day_name {
5244                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
5245                        let Literal::String(s) = lit.as_ref() else {
5246                            unreachable!()
5247                        };
5248                        let upper = s.to_uppercase();
5249                        if upper.starts_with("MO") {
5250                            Some(1)
5251                        } else if upper.starts_with("TU") {
5252                            Some(2)
5253                        } else if upper.starts_with("WE") {
5254                            Some(3)
5255                        } else if upper.starts_with("TH") {
5256                            Some(4)
5257                        } else if upper.starts_with("FR") {
5258                            Some(5)
5259                        } else if upper.starts_with("SA") {
5260                            Some(6)
5261                        } else if upper.starts_with("SU") {
5262                            Some(7)
5263                        } else {
5264                            None
5265                        }
5266                    }
5267                    _ => None,
5268                };
5269
5270                let target_day_expr = if let Some(n) = day_num {
5271                    Expression::number(n)
5272                } else {
5273                    // Dynamic day name: CASE WHEN STARTS_WITH(UPPER(day_column), 'MO') THEN 1 ... END
5274                    Expression::Case(Box::new(Case {
5275                        operand: None,
5276                        whens: vec![
5277                            (
5278                                Expression::Function(Box::new(Function::new(
5279                                    "STARTS_WITH".to_string(),
5280                                    vec![
5281                                        Expression::Upper(Box::new(UnaryFunc::new(
5282                                            day_name.clone(),
5283                                        ))),
5284                                        Expression::Literal(Box::new(Literal::String(
5285                                            "MO".to_string(),
5286                                        ))),
5287                                    ],
5288                                ))),
5289                                Expression::number(1),
5290                            ),
5291                            (
5292                                Expression::Function(Box::new(Function::new(
5293                                    "STARTS_WITH".to_string(),
5294                                    vec![
5295                                        Expression::Upper(Box::new(UnaryFunc::new(
5296                                            day_name.clone(),
5297                                        ))),
5298                                        Expression::Literal(Box::new(Literal::String(
5299                                            "TU".to_string(),
5300                                        ))),
5301                                    ],
5302                                ))),
5303                                Expression::number(2),
5304                            ),
5305                            (
5306                                Expression::Function(Box::new(Function::new(
5307                                    "STARTS_WITH".to_string(),
5308                                    vec![
5309                                        Expression::Upper(Box::new(UnaryFunc::new(
5310                                            day_name.clone(),
5311                                        ))),
5312                                        Expression::Literal(Box::new(Literal::String(
5313                                            "WE".to_string(),
5314                                        ))),
5315                                    ],
5316                                ))),
5317                                Expression::number(3),
5318                            ),
5319                            (
5320                                Expression::Function(Box::new(Function::new(
5321                                    "STARTS_WITH".to_string(),
5322                                    vec![
5323                                        Expression::Upper(Box::new(UnaryFunc::new(
5324                                            day_name.clone(),
5325                                        ))),
5326                                        Expression::Literal(Box::new(Literal::String(
5327                                            "TH".to_string(),
5328                                        ))),
5329                                    ],
5330                                ))),
5331                                Expression::number(4),
5332                            ),
5333                            (
5334                                Expression::Function(Box::new(Function::new(
5335                                    "STARTS_WITH".to_string(),
5336                                    vec![
5337                                        Expression::Upper(Box::new(UnaryFunc::new(
5338                                            day_name.clone(),
5339                                        ))),
5340                                        Expression::Literal(Box::new(Literal::String(
5341                                            "FR".to_string(),
5342                                        ))),
5343                                    ],
5344                                ))),
5345                                Expression::number(5),
5346                            ),
5347                            (
5348                                Expression::Function(Box::new(Function::new(
5349                                    "STARTS_WITH".to_string(),
5350                                    vec![
5351                                        Expression::Upper(Box::new(UnaryFunc::new(
5352                                            day_name.clone(),
5353                                        ))),
5354                                        Expression::Literal(Box::new(Literal::String(
5355                                            "SA".to_string(),
5356                                        ))),
5357                                    ],
5358                                ))),
5359                                Expression::number(6),
5360                            ),
5361                            (
5362                                Expression::Function(Box::new(Function::new(
5363                                    "STARTS_WITH".to_string(),
5364                                    vec![
5365                                        Expression::Upper(Box::new(UnaryFunc::new(day_name))),
5366                                        Expression::Literal(Box::new(Literal::String(
5367                                            "SU".to_string(),
5368                                        ))),
5369                                    ],
5370                                ))),
5371                                Expression::number(7),
5372                            ),
5373                        ],
5374                        else_: None,
5375                        comments: Vec::new(),
5376                        inferred_type: None,
5377                    }))
5378                };
5379
5380                let isodow = Expression::Function(Box::new(Function::new(
5381                    "ISODOW".to_string(),
5382                    vec![date.clone()],
5383                )));
5384                // ((target_day - ISODOW(date) + 6) % 7) + 1
5385                let diff = Expression::Add(Box::new(BinaryOp {
5386                    left: Expression::Paren(Box::new(Paren {
5387                        this: Expression::Mod(Box::new(BinaryOp {
5388                            left: Expression::Paren(Box::new(Paren {
5389                                this: Expression::Add(Box::new(BinaryOp {
5390                                    left: Expression::Paren(Box::new(Paren {
5391                                        this: Expression::Sub(Box::new(BinaryOp {
5392                                            left: target_day_expr,
5393                                            right: isodow,
5394                                            left_comments: Vec::new(),
5395                                            operator_comments: Vec::new(),
5396                                            trailing_comments: Vec::new(),
5397                                            inferred_type: None,
5398                                        })),
5399                                        trailing_comments: Vec::new(),
5400                                    })),
5401                                    right: Expression::number(6),
5402                                    left_comments: Vec::new(),
5403                                    operator_comments: Vec::new(),
5404                                    trailing_comments: Vec::new(),
5405                                    inferred_type: None,
5406                                })),
5407                                trailing_comments: Vec::new(),
5408                            })),
5409                            right: Expression::number(7),
5410                            left_comments: Vec::new(),
5411                            operator_comments: Vec::new(),
5412                            trailing_comments: Vec::new(),
5413                            inferred_type: None,
5414                        })),
5415                        trailing_comments: Vec::new(),
5416                    })),
5417                    right: Expression::number(1),
5418                    left_comments: Vec::new(),
5419                    operator_comments: Vec::new(),
5420                    trailing_comments: Vec::new(),
5421                    inferred_type: None,
5422                }));
5423
5424                let result = Expression::Add(Box::new(BinaryOp {
5425                    left: date,
5426                    right: Expression::Interval(Box::new(Interval {
5427                        this: Some(Expression::Paren(Box::new(Paren {
5428                            this: diff,
5429                            trailing_comments: Vec::new(),
5430                        }))),
5431                        unit: Some(IntervalUnitSpec::Simple {
5432                            unit: IntervalUnit::Day,
5433                            use_plural: false,
5434                        }),
5435                    })),
5436                    left_comments: Vec::new(),
5437                    operator_comments: Vec::new(),
5438                    trailing_comments: Vec::new(),
5439                    inferred_type: None,
5440                }));
5441
5442                Ok(Expression::Cast(Box::new(Cast {
5443                    this: result,
5444                    to: DataType::Date,
5445                    trailing_comments: Vec::new(),
5446                    double_colon_syntax: false,
5447                    format: None,
5448                    default: None,
5449                    inferred_type: None,
5450                })))
5451            }
5452
5453            // PREVIOUS_DAY(date, 'day_name') -> complex expression using ISODOW
5454            "PREVIOUS_DAY" if f.args.len() == 2 => {
5455                let mut args = f.args;
5456                let date = args.remove(0);
5457                let day_name = args.remove(0);
5458
5459                let day_num = match &day_name {
5460                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
5461                        let Literal::String(s) = lit.as_ref() else {
5462                            unreachable!()
5463                        };
5464                        let upper = s.to_uppercase();
5465                        if upper.starts_with("MO") {
5466                            Some(1)
5467                        } else if upper.starts_with("TU") {
5468                            Some(2)
5469                        } else if upper.starts_with("WE") {
5470                            Some(3)
5471                        } else if upper.starts_with("TH") {
5472                            Some(4)
5473                        } else if upper.starts_with("FR") {
5474                            Some(5)
5475                        } else if upper.starts_with("SA") {
5476                            Some(6)
5477                        } else if upper.starts_with("SU") {
5478                            Some(7)
5479                        } else {
5480                            None
5481                        }
5482                    }
5483                    _ => None,
5484                };
5485
5486                let target_day_expr = if let Some(n) = day_num {
5487                    Expression::number(n)
5488                } else {
5489                    Expression::Case(Box::new(Case {
5490                        operand: None,
5491                        whens: vec![
5492                            (
5493                                Expression::Function(Box::new(Function::new(
5494                                    "STARTS_WITH".to_string(),
5495                                    vec![
5496                                        Expression::Upper(Box::new(UnaryFunc::new(
5497                                            day_name.clone(),
5498                                        ))),
5499                                        Expression::Literal(Box::new(Literal::String(
5500                                            "MO".to_string(),
5501                                        ))),
5502                                    ],
5503                                ))),
5504                                Expression::number(1),
5505                            ),
5506                            (
5507                                Expression::Function(Box::new(Function::new(
5508                                    "STARTS_WITH".to_string(),
5509                                    vec![
5510                                        Expression::Upper(Box::new(UnaryFunc::new(
5511                                            day_name.clone(),
5512                                        ))),
5513                                        Expression::Literal(Box::new(Literal::String(
5514                                            "TU".to_string(),
5515                                        ))),
5516                                    ],
5517                                ))),
5518                                Expression::number(2),
5519                            ),
5520                            (
5521                                Expression::Function(Box::new(Function::new(
5522                                    "STARTS_WITH".to_string(),
5523                                    vec![
5524                                        Expression::Upper(Box::new(UnaryFunc::new(
5525                                            day_name.clone(),
5526                                        ))),
5527                                        Expression::Literal(Box::new(Literal::String(
5528                                            "WE".to_string(),
5529                                        ))),
5530                                    ],
5531                                ))),
5532                                Expression::number(3),
5533                            ),
5534                            (
5535                                Expression::Function(Box::new(Function::new(
5536                                    "STARTS_WITH".to_string(),
5537                                    vec![
5538                                        Expression::Upper(Box::new(UnaryFunc::new(
5539                                            day_name.clone(),
5540                                        ))),
5541                                        Expression::Literal(Box::new(Literal::String(
5542                                            "TH".to_string(),
5543                                        ))),
5544                                    ],
5545                                ))),
5546                                Expression::number(4),
5547                            ),
5548                            (
5549                                Expression::Function(Box::new(Function::new(
5550                                    "STARTS_WITH".to_string(),
5551                                    vec![
5552                                        Expression::Upper(Box::new(UnaryFunc::new(
5553                                            day_name.clone(),
5554                                        ))),
5555                                        Expression::Literal(Box::new(Literal::String(
5556                                            "FR".to_string(),
5557                                        ))),
5558                                    ],
5559                                ))),
5560                                Expression::number(5),
5561                            ),
5562                            (
5563                                Expression::Function(Box::new(Function::new(
5564                                    "STARTS_WITH".to_string(),
5565                                    vec![
5566                                        Expression::Upper(Box::new(UnaryFunc::new(
5567                                            day_name.clone(),
5568                                        ))),
5569                                        Expression::Literal(Box::new(Literal::String(
5570                                            "SA".to_string(),
5571                                        ))),
5572                                    ],
5573                                ))),
5574                                Expression::number(6),
5575                            ),
5576                            (
5577                                Expression::Function(Box::new(Function::new(
5578                                    "STARTS_WITH".to_string(),
5579                                    vec![
5580                                        Expression::Upper(Box::new(UnaryFunc::new(day_name))),
5581                                        Expression::Literal(Box::new(Literal::String(
5582                                            "SU".to_string(),
5583                                        ))),
5584                                    ],
5585                                ))),
5586                                Expression::number(7),
5587                            ),
5588                        ],
5589                        else_: None,
5590                        comments: Vec::new(),
5591                        inferred_type: None,
5592                    }))
5593                };
5594
5595                let isodow = Expression::Function(Box::new(Function::new(
5596                    "ISODOW".to_string(),
5597                    vec![date.clone()],
5598                )));
5599                // ((ISODOW(date) - target_day + 6) % 7) + 1
5600                let diff = Expression::Add(Box::new(BinaryOp {
5601                    left: Expression::Paren(Box::new(Paren {
5602                        this: Expression::Mod(Box::new(BinaryOp {
5603                            left: Expression::Paren(Box::new(Paren {
5604                                this: Expression::Add(Box::new(BinaryOp {
5605                                    left: Expression::Paren(Box::new(Paren {
5606                                        this: Expression::Sub(Box::new(BinaryOp {
5607                                            left: isodow,
5608                                            right: target_day_expr,
5609                                            left_comments: Vec::new(),
5610                                            operator_comments: Vec::new(),
5611                                            trailing_comments: Vec::new(),
5612                                            inferred_type: None,
5613                                        })),
5614                                        trailing_comments: Vec::new(),
5615                                    })),
5616                                    right: Expression::number(6),
5617                                    left_comments: Vec::new(),
5618                                    operator_comments: Vec::new(),
5619                                    trailing_comments: Vec::new(),
5620                                    inferred_type: None,
5621                                })),
5622                                trailing_comments: Vec::new(),
5623                            })),
5624                            right: Expression::number(7),
5625                            left_comments: Vec::new(),
5626                            operator_comments: Vec::new(),
5627                            trailing_comments: Vec::new(),
5628                            inferred_type: None,
5629                        })),
5630                        trailing_comments: Vec::new(),
5631                    })),
5632                    right: Expression::number(1),
5633                    left_comments: Vec::new(),
5634                    operator_comments: Vec::new(),
5635                    trailing_comments: Vec::new(),
5636                    inferred_type: None,
5637                }));
5638
5639                let result = Expression::Sub(Box::new(BinaryOp {
5640                    left: date,
5641                    right: Expression::Interval(Box::new(Interval {
5642                        this: Some(Expression::Paren(Box::new(Paren {
5643                            this: diff,
5644                            trailing_comments: Vec::new(),
5645                        }))),
5646                        unit: Some(IntervalUnitSpec::Simple {
5647                            unit: IntervalUnit::Day,
5648                            use_plural: false,
5649                        }),
5650                    })),
5651                    left_comments: Vec::new(),
5652                    operator_comments: Vec::new(),
5653                    trailing_comments: Vec::new(),
5654                    inferred_type: None,
5655                }));
5656
5657                Ok(Expression::Cast(Box::new(Cast {
5658                    this: result,
5659                    to: DataType::Date,
5660                    trailing_comments: Vec::new(),
5661                    double_colon_syntax: false,
5662                    format: None,
5663                    default: None,
5664                    inferred_type: None,
5665                })))
5666            }
5667
5668            // LAST_DAY(date, YEAR) -> MAKE_DATE(EXTRACT(YEAR FROM date), 12, 31)
5669            // LAST_DAY(date, QUARTER) -> LAST_DAY(MAKE_DATE(EXTRACT(YEAR FROM date), EXTRACT(QUARTER FROM date) * 3, 1))
5670            // LAST_DAY(date, WEEK) -> CAST(date + INTERVAL ((7 - EXTRACT(DAYOFWEEK FROM date)) % 7) DAY AS DATE)
5671            "LAST_DAY" if f.args.len() == 2 => {
5672                let mut args = f.args;
5673                let date = args.remove(0);
5674                let unit = args.remove(0);
5675                let unit_str = match &unit {
5676                    Expression::Column(c) => c.name.name.to_uppercase(),
5677                    Expression::Identifier(i) => i.name.to_uppercase(),
5678                    _ => String::new(),
5679                };
5680
5681                match unit_str.as_str() {
5682                    "MONTH" => Ok(Expression::Function(Box::new(Function::new(
5683                        "LAST_DAY".to_string(),
5684                        vec![date],
5685                    )))),
5686                    "YEAR" => Ok(Expression::Function(Box::new(Function::new(
5687                        "MAKE_DATE".to_string(),
5688                        vec![
5689                            Expression::Extract(Box::new(crate::expressions::ExtractFunc {
5690                                this: date,
5691                                field: crate::expressions::DateTimeField::Year,
5692                            })),
5693                            Expression::number(12),
5694                            Expression::number(31),
5695                        ],
5696                    )))),
5697                    "QUARTER" => {
5698                        let year = Expression::Extract(Box::new(crate::expressions::ExtractFunc {
5699                            this: date.clone(),
5700                            field: crate::expressions::DateTimeField::Year,
5701                        }));
5702                        let quarter_month = Expression::Mul(Box::new(BinaryOp {
5703                            left: Expression::Extract(Box::new(crate::expressions::ExtractFunc {
5704                                this: date,
5705                                field: crate::expressions::DateTimeField::Custom(
5706                                    "QUARTER".to_string(),
5707                                ),
5708                            })),
5709                            right: Expression::number(3),
5710                            left_comments: Vec::new(),
5711                            operator_comments: Vec::new(),
5712                            trailing_comments: Vec::new(),
5713                            inferred_type: None,
5714                        }));
5715                        let make_date = Expression::Function(Box::new(Function::new(
5716                            "MAKE_DATE".to_string(),
5717                            vec![year, quarter_month, Expression::number(1)],
5718                        )));
5719                        Ok(Expression::Function(Box::new(Function::new(
5720                            "LAST_DAY".to_string(),
5721                            vec![make_date],
5722                        ))))
5723                    }
5724                    "WEEK" => {
5725                        let dow = Expression::Extract(Box::new(crate::expressions::ExtractFunc {
5726                            this: date.clone(),
5727                            field: crate::expressions::DateTimeField::Custom(
5728                                "DAYOFWEEK".to_string(),
5729                            ),
5730                        }));
5731                        let diff = Expression::Mod(Box::new(BinaryOp {
5732                            left: Expression::Paren(Box::new(Paren {
5733                                this: Expression::Sub(Box::new(BinaryOp {
5734                                    left: Expression::number(7),
5735                                    right: dow,
5736                                    left_comments: Vec::new(),
5737                                    operator_comments: Vec::new(),
5738                                    trailing_comments: Vec::new(),
5739                                    inferred_type: None,
5740                                })),
5741                                trailing_comments: Vec::new(),
5742                            })),
5743                            right: Expression::number(7),
5744                            left_comments: Vec::new(),
5745                            operator_comments: Vec::new(),
5746                            trailing_comments: Vec::new(),
5747                            inferred_type: None,
5748                        }));
5749                        let result = Expression::Add(Box::new(BinaryOp {
5750                            left: date,
5751                            right: Expression::Interval(Box::new(Interval {
5752                                this: Some(Expression::Paren(Box::new(Paren {
5753                                    this: diff,
5754                                    trailing_comments: Vec::new(),
5755                                }))),
5756                                unit: Some(IntervalUnitSpec::Simple {
5757                                    unit: IntervalUnit::Day,
5758                                    use_plural: false,
5759                                }),
5760                            })),
5761                            left_comments: Vec::new(),
5762                            operator_comments: Vec::new(),
5763                            trailing_comments: Vec::new(),
5764                            inferred_type: None,
5765                        }));
5766                        Ok(Expression::Cast(Box::new(Cast {
5767                            this: result,
5768                            to: DataType::Date,
5769                            trailing_comments: Vec::new(),
5770                            double_colon_syntax: false,
5771                            format: None,
5772                            default: None,
5773                            inferred_type: None,
5774                        })))
5775                    }
5776                    _ => Ok(Expression::Function(Box::new(Function::new(
5777                        "LAST_DAY".to_string(),
5778                        vec![date, unit],
5779                    )))),
5780                }
5781            }
5782
5783            // SEQ1/SEQ2/SEQ4/SEQ8 -> (ROW_NUMBER() OVER (ORDER BY 1 NULLS FIRST) - 1) % range
5784            // When FROM clause is RANGE(n), a post-transform replaces this with `range % N`
5785            "SEQ1" | "SEQ2" | "SEQ4" | "SEQ8" => {
5786                let (range, half): (u128, u128) = match name_upper.as_str() {
5787                    "SEQ1" => (256, 128),
5788                    "SEQ2" => (65536, 32768),
5789                    "SEQ4" => (4294967296, 2147483648),
5790                    "SEQ8" => (18446744073709551616, 9223372036854775808),
5791                    _ => unreachable!("sequence type already matched in caller"),
5792                };
5793
5794                let is_signed = match f.args.first() {
5795                    Some(Expression::Literal(lit))
5796                        if matches!(lit.as_ref(), Literal::Number(_)) =>
5797                    {
5798                        let Literal::Number(n) = lit.as_ref() else {
5799                            unreachable!()
5800                        };
5801                        n == "1"
5802                    }
5803                    _ => false,
5804                };
5805
5806                let row_num = Expression::Sub(Box::new(BinaryOp {
5807                    left: Expression::WindowFunction(Box::new(
5808                        crate::expressions::WindowFunction {
5809                            this: Expression::Function(Box::new(Function::new(
5810                                "ROW_NUMBER".to_string(),
5811                                vec![],
5812                            ))),
5813                            over: crate::expressions::Over {
5814                                window_name: None,
5815                                partition_by: vec![],
5816                                order_by: vec![crate::expressions::Ordered {
5817                                    nulls_auto: false,
5818                                    this: Expression::number(1),
5819                                    desc: false,
5820                                    nulls_first: Some(true),
5821                                    explicit_asc: false,
5822                                    with_fill: None,
5823                                }],
5824                                frame: None,
5825                                alias: None,
5826                            },
5827                            keep: None,
5828                            inferred_type: None,
5829                        },
5830                    )),
5831                    right: Expression::number(1),
5832                    left_comments: Vec::new(),
5833                    operator_comments: Vec::new(),
5834                    trailing_comments: Vec::new(),
5835                    inferred_type: None,
5836                }));
5837
5838                let modded = Expression::Mod(Box::new(BinaryOp {
5839                    left: Expression::Paren(Box::new(Paren {
5840                        this: row_num,
5841                        trailing_comments: Vec::new(),
5842                    })),
5843                    right: Expression::Literal(Box::new(Literal::Number(range.to_string()))),
5844                    left_comments: Vec::new(),
5845                    operator_comments: Vec::new(),
5846                    trailing_comments: Vec::new(),
5847                    inferred_type: None,
5848                }));
5849
5850                if is_signed {
5851                    // CASE WHEN val >= half THEN val - range ELSE val END
5852                    let cond = Expression::Gte(Box::new(BinaryOp {
5853                        left: modded.clone(),
5854                        right: Expression::Literal(Box::new(Literal::Number(half.to_string()))),
5855                        left_comments: Vec::new(),
5856                        operator_comments: Vec::new(),
5857                        trailing_comments: Vec::new(),
5858                        inferred_type: None,
5859                    }));
5860                    let signed_val = Expression::Sub(Box::new(BinaryOp {
5861                        left: modded.clone(),
5862                        right: Expression::Literal(Box::new(Literal::Number(range.to_string()))),
5863                        left_comments: Vec::new(),
5864                        operator_comments: Vec::new(),
5865                        trailing_comments: Vec::new(),
5866                        inferred_type: None,
5867                    }));
5868                    Ok(Expression::Paren(Box::new(Paren {
5869                        this: Expression::Case(Box::new(Case {
5870                            operand: None,
5871                            whens: vec![(cond, signed_val)],
5872                            else_: Some(modded),
5873                            comments: Vec::new(),
5874                            inferred_type: None,
5875                        })),
5876                        trailing_comments: Vec::new(),
5877                    })))
5878                } else {
5879                    Ok(modded)
5880                }
5881            }
5882
5883            // TABLE(fn) -> fn (unwrap TABLE wrapper for DuckDB)
5884            // Also handles TABLE(GENERATOR(ROWCOUNT => n)) -> RANGE(n) directly
5885            "TABLE" if f.args.len() == 1 => {
5886                let inner = f.args.into_iter().next().unwrap();
5887                // If inner is GENERATOR, transform it to RANGE
5888                if let Expression::Function(ref gen_f) = inner {
5889                    if gen_f.name.to_uppercase() == "GENERATOR" {
5890                        let mut rowcount = None;
5891                        for arg in &gen_f.args {
5892                            if let Expression::NamedArgument(na) = arg {
5893                                if na.name.name.to_uppercase() == "ROWCOUNT" {
5894                                    rowcount = Some(na.value.clone());
5895                                }
5896                            }
5897                        }
5898                        if let Some(n) = rowcount {
5899                            return Ok(Expression::Function(Box::new(Function::new(
5900                                "RANGE".to_string(),
5901                                vec![n],
5902                            ))));
5903                        }
5904                    }
5905                }
5906                Ok(inner)
5907            }
5908
5909            // GENERATOR(ROWCOUNT => n) -> RANGE(n) in DuckDB
5910            "GENERATOR" => {
5911                let mut rowcount = None;
5912                for arg in &f.args {
5913                    if let Expression::NamedArgument(na) = arg {
5914                        if na.name.name.to_uppercase() == "ROWCOUNT" {
5915                            rowcount = Some(na.value.clone());
5916                        }
5917                    }
5918                }
5919                if let Some(n) = rowcount {
5920                    Ok(Expression::Function(Box::new(Function::new(
5921                        "RANGE".to_string(),
5922                        vec![n],
5923                    ))))
5924                } else {
5925                    Ok(Expression::Function(Box::new(f)))
5926                }
5927            }
5928
5929            // UNIFORM(low, high, gen) -> CAST(FLOOR(low + RANDOM() * (high - low + 1)) AS BIGINT)
5930            // or with seed: CAST(FLOOR(low + (ABS(HASH(seed)) % 1000000) / 1000000.0 * (high - low + 1)) AS BIGINT)
5931            "UNIFORM" if f.args.len() == 3 => {
5932                let mut args = f.args;
5933                let low = args.remove(0);
5934                let high = args.remove(0);
5935                let gen = args.remove(0);
5936
5937                let range = Expression::Add(Box::new(BinaryOp {
5938                    left: Expression::Sub(Box::new(BinaryOp {
5939                        left: high,
5940                        right: low.clone(),
5941                        left_comments: Vec::new(),
5942                        operator_comments: Vec::new(),
5943                        trailing_comments: Vec::new(),
5944                        inferred_type: None,
5945                    })),
5946                    right: Expression::number(1),
5947                    left_comments: Vec::new(),
5948                    operator_comments: Vec::new(),
5949                    trailing_comments: Vec::new(),
5950                    inferred_type: None,
5951                }));
5952
5953                // Check if gen is RANDOM() (function) or a literal seed
5954                let random_val = match &gen {
5955                    Expression::Rand(_) | Expression::Random(_) => {
5956                        // RANDOM() - use directly
5957                        Expression::Function(Box::new(Function::new("RANDOM".to_string(), vec![])))
5958                    }
5959                    Expression::Function(func) if func.name.to_uppercase() == "RANDOM" => {
5960                        // RANDOM(seed) or RANDOM() - just use RANDOM()
5961                        Expression::Function(Box::new(Function::new("RANDOM".to_string(), vec![])))
5962                    }
5963                    _ => {
5964                        // Seed-based: (ABS(HASH(seed)) % 1000000) / 1000000.0
5965                        let hash = Expression::Function(Box::new(Function::new(
5966                            "HASH".to_string(),
5967                            vec![gen],
5968                        )));
5969                        let abs_hash = Expression::Abs(Box::new(UnaryFunc::new(hash)));
5970                        let modded = Expression::Mod(Box::new(BinaryOp {
5971                            left: abs_hash,
5972                            right: Expression::number(1000000),
5973                            left_comments: Vec::new(),
5974                            operator_comments: Vec::new(),
5975                            trailing_comments: Vec::new(),
5976                            inferred_type: None,
5977                        }));
5978                        let paren_modded = Expression::Paren(Box::new(Paren {
5979                            this: modded,
5980                            trailing_comments: Vec::new(),
5981                        }));
5982                        Expression::Div(Box::new(BinaryOp {
5983                            left: paren_modded,
5984                            right: Expression::Literal(Box::new(Literal::Number(
5985                                "1000000.0".to_string(),
5986                            ))),
5987                            left_comments: Vec::new(),
5988                            operator_comments: Vec::new(),
5989                            trailing_comments: Vec::new(),
5990                            inferred_type: None,
5991                        }))
5992                    }
5993                };
5994
5995                let inner = Expression::Function(Box::new(Function::new(
5996                    "FLOOR".to_string(),
5997                    vec![Expression::Add(Box::new(BinaryOp {
5998                        left: low,
5999                        right: Expression::Mul(Box::new(BinaryOp {
6000                            left: random_val,
6001                            right: Expression::Paren(Box::new(Paren {
6002                                this: range,
6003                                trailing_comments: Vec::new(),
6004                            })),
6005                            left_comments: Vec::new(),
6006                            operator_comments: Vec::new(),
6007                            trailing_comments: Vec::new(),
6008                            inferred_type: None,
6009                        })),
6010                        left_comments: Vec::new(),
6011                        operator_comments: Vec::new(),
6012                        trailing_comments: Vec::new(),
6013                        inferred_type: None,
6014                    }))],
6015                )));
6016
6017                Ok(Expression::Cast(Box::new(Cast {
6018                    this: inner,
6019                    to: DataType::BigInt { length: None },
6020                    trailing_comments: Vec::new(),
6021                    double_colon_syntax: false,
6022                    format: None,
6023                    default: None,
6024                    inferred_type: None,
6025                })))
6026            }
6027
6028            // NORMAL(mean, stddev, gen) -> Box-Muller transform
6029            // mean + (stddev * SQRT(-2 * LN(GREATEST(u1, 1e-10))) * COS(2 * PI() * u2))
6030            // where u1 and u2 are uniform random values derived from gen
6031            "NORMAL" if f.args.len() == 3 => {
6032                let mut args = f.args;
6033                let mean = args.remove(0);
6034                let stddev = args.remove(0);
6035                let gen = args.remove(0);
6036
6037                // Helper to create seed-based random: (ABS(HASH(seed)) % 1000000) / 1000000.0
6038                let make_seed_random = |seed: Expression| -> Expression {
6039                    let hash = Expression::Function(Box::new(Function::new(
6040                        "HASH".to_string(),
6041                        vec![seed],
6042                    )));
6043                    let abs_hash = Expression::Abs(Box::new(UnaryFunc::new(hash)));
6044                    let modded = Expression::Mod(Box::new(BinaryOp {
6045                        left: abs_hash,
6046                        right: Expression::number(1000000),
6047                        left_comments: Vec::new(),
6048                        operator_comments: Vec::new(),
6049                        trailing_comments: Vec::new(),
6050                        inferred_type: None,
6051                    }));
6052                    let paren_modded = Expression::Paren(Box::new(Paren {
6053                        this: modded,
6054                        trailing_comments: Vec::new(),
6055                    }));
6056                    Expression::Div(Box::new(BinaryOp {
6057                        left: paren_modded,
6058                        right: Expression::Literal(Box::new(Literal::Number(
6059                            "1000000.0".to_string(),
6060                        ))),
6061                        left_comments: Vec::new(),
6062                        operator_comments: Vec::new(),
6063                        trailing_comments: Vec::new(),
6064                        inferred_type: None,
6065                    }))
6066                };
6067
6068                // Determine u1 and u2 based on gen type
6069                let is_random_no_seed = match &gen {
6070                    Expression::Random(_) => true,
6071                    Expression::Rand(r) => r.seed.is_none(),
6072                    _ => false,
6073                };
6074                let (u1, u2) = if is_random_no_seed {
6075                    // RANDOM() -> u1 = RANDOM(), u2 = RANDOM()
6076                    let u1 =
6077                        Expression::Function(Box::new(Function::new("RANDOM".to_string(), vec![])));
6078                    let u2 =
6079                        Expression::Function(Box::new(Function::new("RANDOM".to_string(), vec![])));
6080                    (u1, u2)
6081                } else {
6082                    // Seed-based: extract the seed value
6083                    let seed = match gen {
6084                        Expression::Rand(r) => r.seed.map(|s| *s).unwrap_or(Expression::number(0)),
6085                        Expression::Function(func) if func.name.to_uppercase() == "RANDOM" => {
6086                            if func.args.len() == 1 {
6087                                func.args.into_iter().next().unwrap()
6088                            } else {
6089                                Expression::number(0)
6090                            }
6091                        }
6092                        other => other,
6093                    };
6094                    let u1 = make_seed_random(seed.clone());
6095                    let seed_plus_1 = Expression::Add(Box::new(BinaryOp {
6096                        left: seed,
6097                        right: Expression::number(1),
6098                        left_comments: Vec::new(),
6099                        operator_comments: Vec::new(),
6100                        trailing_comments: Vec::new(),
6101                        inferred_type: None,
6102                    }));
6103                    let u2 = make_seed_random(seed_plus_1);
6104                    (u1, u2)
6105                };
6106
6107                // GREATEST(u1, 1e-10)
6108                let greatest = Expression::Greatest(Box::new(VarArgFunc {
6109                    expressions: vec![
6110                        u1,
6111                        Expression::Literal(Box::new(Literal::Number("1e-10".to_string()))),
6112                    ],
6113                    original_name: None,
6114                    inferred_type: None,
6115                }));
6116
6117                // SQRT(-2 * LN(GREATEST(u1, 1e-10)))
6118                let neg2 = Expression::Neg(Box::new(crate::expressions::UnaryOp {
6119                    this: Expression::number(2),
6120                    inferred_type: None,
6121                }));
6122                let ln_greatest =
6123                    Expression::Function(Box::new(Function::new("LN".to_string(), vec![greatest])));
6124                let neg2_times_ln = Expression::Mul(Box::new(BinaryOp {
6125                    left: neg2,
6126                    right: ln_greatest,
6127                    left_comments: Vec::new(),
6128                    operator_comments: Vec::new(),
6129                    trailing_comments: Vec::new(),
6130                    inferred_type: None,
6131                }));
6132                let sqrt_part = Expression::Function(Box::new(Function::new(
6133                    "SQRT".to_string(),
6134                    vec![neg2_times_ln],
6135                )));
6136
6137                // COS(2 * PI() * u2)
6138                let pi = Expression::Function(Box::new(Function::new("PI".to_string(), vec![])));
6139                let two_pi = Expression::Mul(Box::new(BinaryOp {
6140                    left: Expression::number(2),
6141                    right: pi,
6142                    left_comments: Vec::new(),
6143                    operator_comments: Vec::new(),
6144                    trailing_comments: Vec::new(),
6145                    inferred_type: None,
6146                }));
6147                let two_pi_u2 = if let Expression::Div(div) = u2 {
6148                    let numerator = Expression::Mul(Box::new(BinaryOp::new(two_pi, div.left)));
6149                    Expression::Div(Box::new(BinaryOp::new(numerator, div.right)))
6150                } else {
6151                    Expression::Mul(Box::new(BinaryOp::new(two_pi, u2)))
6152                };
6153                let cos_part = Expression::Function(Box::new(Function::new(
6154                    "COS".to_string(),
6155                    vec![two_pi_u2],
6156                )));
6157
6158                // stddev * sqrt_part * cos_part
6159                let stddev_times_sqrt = Expression::Mul(Box::new(BinaryOp {
6160                    left: stddev,
6161                    right: sqrt_part,
6162                    left_comments: Vec::new(),
6163                    operator_comments: Vec::new(),
6164                    trailing_comments: Vec::new(),
6165                    inferred_type: None,
6166                }));
6167                let inner = Expression::Mul(Box::new(BinaryOp {
6168                    left: stddev_times_sqrt,
6169                    right: cos_part,
6170                    left_comments: Vec::new(),
6171                    operator_comments: Vec::new(),
6172                    trailing_comments: Vec::new(),
6173                    inferred_type: None,
6174                }));
6175                let paren_inner = Expression::Paren(Box::new(Paren {
6176                    this: inner,
6177                    trailing_comments: Vec::new(),
6178                }));
6179
6180                // mean + (inner)
6181                Ok(Expression::Add(Box::new(BinaryOp {
6182                    left: mean,
6183                    right: paren_inner,
6184                    left_comments: Vec::new(),
6185                    operator_comments: Vec::new(),
6186                    trailing_comments: Vec::new(),
6187                    inferred_type: None,
6188                })))
6189            }
6190
6191            // DATE_TRUNC: DuckDB supports natively, just pass through
6192            // (DuckDB returns the correct type automatically)
6193
6194            // BITOR/BITAND with BITSHIFT need parenthesization
6195            // This is handled via the BITOR/BITAND transforms which create BitwiseOr/BitwiseAnd
6196            // The issue is operator precedence: BITOR(BITSHIFTLEFT(a, b), BITSHIFTLEFT(c, d))
6197            // should generate (a << b) | (c << d), not a << b | c << d
6198
6199            // ZIPF(s, n, gen) -> CTE-based emulation for DuckDB
6200            "ZIPF" if f.args.len() == 3 => {
6201                let mut args = f.args;
6202                let s_expr = args.remove(0);
6203                let n_expr = args.remove(0);
6204                let gen_expr = args.remove(0);
6205
6206                let s_sql = Self::expr_to_sql(&s_expr);
6207                let n_sql = Self::expr_to_sql(&n_expr);
6208                let (seed_sql, is_random) = Self::extract_seed_info(&gen_expr);
6209
6210                let rand_sql = if is_random {
6211                    format!("SELECT {} AS r", seed_sql)
6212                } else {
6213                    format!(
6214                        "SELECT (ABS(HASH({})) % 1000000) / 1000000.0 AS r",
6215                        seed_sql
6216                    )
6217                };
6218
6219                let template = format!(
6220                    "WITH rand AS ({}), weights AS (SELECT i, 1.0 / POWER(i, {}) AS w FROM RANGE(1, {} + 1) AS t(i)), cdf AS (SELECT i, SUM(w) OVER (ORDER BY i NULLS FIRST) / SUM(w) OVER () AS p FROM weights) SELECT MIN(i) FROM cdf WHERE p >= (SELECT r FROM rand)",
6221                    rand_sql, s_sql, n_sql
6222                );
6223
6224                Self::parse_as_subquery(&template)
6225            }
6226
6227            // RANDSTR(len, gen) -> subquery-based emulation for DuckDB
6228            "RANDSTR" if f.args.len() == 2 => {
6229                let mut args = f.args;
6230                let len_expr = args.remove(0);
6231                let gen_expr = args.remove(0);
6232
6233                let len_sql = Self::expr_to_sql(&len_expr);
6234                let (seed_sql, is_random) = Self::extract_seed_info(&gen_expr);
6235
6236                let random_value_sql = if is_random {
6237                    format!("(ABS(HASH(i + {})) % 1000) / 1000.0", seed_sql)
6238                } else {
6239                    format!("(ABS(HASH(i + {})) % 1000) / 1000.0", seed_sql)
6240                };
6241
6242                let template = format!(
6243                    "SELECT LISTAGG(SUBSTRING('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 1 + CAST(FLOOR(random_value * 62) AS INT), 1), '') FROM (SELECT {} AS random_value FROM RANGE({}) AS t(i))",
6244                    random_value_sql, len_sql
6245                );
6246
6247                Self::parse_as_subquery(&template)
6248            }
6249
6250            // MAP_CAT(map1, map2) -> explicit merge semantics for DuckDB
6251            "MAP_CAT" if f.args.len() == 2 => {
6252                let mut args = f.args;
6253                let left = Self::normalize_empty_map_expr(args.remove(0));
6254                let right = Self::normalize_empty_map_expr(args.remove(0));
6255                let left_is_null = Expression::IsNull(Box::new(crate::expressions::IsNull {
6256                    this: left.clone(),
6257                    not: false,
6258                    postfix_form: false,
6259                }));
6260                let right_is_null = Expression::IsNull(Box::new(crate::expressions::IsNull {
6261                    this: right.clone(),
6262                    not: false,
6263                    postfix_form: false,
6264                }));
6265                let null_cond = Expression::Or(Box::new(BinaryOp {
6266                    left: left_is_null,
6267                    right: right_is_null,
6268                    left_comments: Vec::new(),
6269                    operator_comments: Vec::new(),
6270                    trailing_comments: Vec::new(),
6271                    inferred_type: None,
6272                }));
6273
6274                let list_concat = Expression::Function(Box::new(Function::new(
6275                    "LIST_CONCAT".to_string(),
6276                    vec![
6277                        Expression::Function(Box::new(Function::new(
6278                            "MAP_KEYS".to_string(),
6279                            vec![left.clone()],
6280                        ))),
6281                        Expression::Function(Box::new(Function::new(
6282                            "MAP_KEYS".to_string(),
6283                            vec![right.clone()],
6284                        ))),
6285                    ],
6286                )));
6287                let list_distinct = Expression::Function(Box::new(Function::new(
6288                    "LIST_DISTINCT".to_string(),
6289                    vec![list_concat],
6290                )));
6291
6292                let k_ident = Identifier::new("__k");
6293                let k_ref = Expression::boxed_column(Column {
6294                    table: None,
6295                    name: k_ident.clone(),
6296                    join_mark: false,
6297                    trailing_comments: Vec::new(),
6298                    span: None,
6299                    inferred_type: None,
6300                });
6301                let right_key = Expression::Subscript(Box::new(crate::expressions::Subscript {
6302                    this: right.clone(),
6303                    index: k_ref.clone(),
6304                }));
6305                let left_key = Expression::Subscript(Box::new(crate::expressions::Subscript {
6306                    this: left.clone(),
6307                    index: k_ref.clone(),
6308                }));
6309                let key_value = Expression::Coalesce(Box::new(VarArgFunc {
6310                    expressions: vec![right_key, left_key],
6311                    original_name: None,
6312                    inferred_type: None,
6313                }));
6314                let struct_pack = Expression::Function(Box::new(Function::new(
6315                    "STRUCT_PACK".to_string(),
6316                    vec![
6317                        Expression::NamedArgument(Box::new(crate::expressions::NamedArgument {
6318                            name: Identifier::new("key"),
6319                            value: k_ref.clone(),
6320                            separator: crate::expressions::NamedArgSeparator::ColonEq,
6321                        })),
6322                        Expression::NamedArgument(Box::new(crate::expressions::NamedArgument {
6323                            name: Identifier::new("value"),
6324                            value: key_value,
6325                            separator: crate::expressions::NamedArgSeparator::ColonEq,
6326                        })),
6327                    ],
6328                )));
6329                let lambda_k = Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
6330                    parameters: vec![k_ident],
6331                    body: struct_pack,
6332                    colon: false,
6333                    parameter_types: Vec::new(),
6334                }));
6335
6336                let list_transform = Expression::Function(Box::new(Function::new(
6337                    "LIST_TRANSFORM".to_string(),
6338                    vec![list_distinct, lambda_k],
6339                )));
6340
6341                let x_ident = Identifier::new("__x");
6342                let x_ref = Expression::boxed_column(Column {
6343                    table: None,
6344                    name: x_ident.clone(),
6345                    join_mark: false,
6346                    trailing_comments: Vec::new(),
6347                    span: None,
6348                    inferred_type: None,
6349                });
6350                let x_value = Expression::Dot(Box::new(crate::expressions::DotAccess {
6351                    this: x_ref,
6352                    field: Identifier::new("value"),
6353                    inferred_type: None,
6354                }));
6355                let x_value_is_null = Expression::IsNull(Box::new(crate::expressions::IsNull {
6356                    this: x_value,
6357                    not: false,
6358                    postfix_form: false,
6359                }));
6360                let lambda_x = Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
6361                    parameters: vec![x_ident],
6362                    body: Expression::Not(Box::new(crate::expressions::UnaryOp {
6363                        this: x_value_is_null,
6364                        inferred_type: None,
6365                    })),
6366                    colon: false,
6367                    parameter_types: Vec::new(),
6368                }));
6369
6370                let list_filter = Expression::Function(Box::new(Function::new(
6371                    "LIST_FILTER".to_string(),
6372                    vec![list_transform, lambda_x],
6373                )));
6374                let merged_map = Expression::Function(Box::new(Function::new(
6375                    "MAP_FROM_ENTRIES".to_string(),
6376                    vec![list_filter],
6377                )));
6378
6379                Ok(Expression::Case(Box::new(Case {
6380                    operand: None,
6381                    whens: vec![(null_cond, Expression::Null(crate::expressions::Null))],
6382                    else_: Some(merged_map),
6383                    comments: Vec::new(),
6384                    inferred_type: None,
6385                })))
6386            }
6387
6388            // MINHASH(num_perm, value) -> DuckDB emulation using JSON state payload
6389            "MINHASH" if f.args.len() == 2 => {
6390                let mut args = f.args;
6391                let num_perm = args.remove(0);
6392                let value = args.remove(0);
6393
6394                let num_perm_sql = Self::expr_to_sql(&num_perm);
6395                let value_sql = Self::expr_to_sql(&value);
6396
6397                let template = format!(
6398                    "SELECT JSON_OBJECT('state', LIST(min_h ORDER BY seed NULLS FIRST), 'type', 'minhash', 'version', 1) FROM (SELECT seed, LIST_MIN(LIST_TRANSFORM(vals, __v -> HASH(CAST(__v AS TEXT) || CAST(seed AS TEXT)))) AS min_h FROM (SELECT LIST({value}) AS vals), RANGE(0, {num_perm}) AS t(seed))",
6399                    value = value_sql,
6400                    num_perm = num_perm_sql
6401                );
6402
6403                Self::parse_as_subquery(&template)
6404            }
6405
6406            // MINHASH_COMBINE(sig) -> merge minhash JSON signatures in DuckDB
6407            "MINHASH_COMBINE" if f.args.len() == 1 => {
6408                let sig_sql = Self::expr_to_sql(&f.args[0]);
6409                let template = format!(
6410                    "SELECT JSON_OBJECT('state', LIST(min_h ORDER BY idx NULLS FIRST), 'type', 'minhash', 'version', 1) FROM (SELECT pos AS idx, MIN(val) AS min_h FROM UNNEST(LIST({sig})) AS _(sig) JOIN UNNEST(CAST(sig -> '$.state' AS UBIGINT[])) WITH ORDINALITY AS t(val, pos) ON TRUE GROUP BY pos)",
6411                    sig = sig_sql
6412                );
6413                Self::parse_as_subquery(&template)
6414            }
6415
6416            // APPROXIMATE_SIMILARITY(sig) -> jaccard estimate from minhash signatures
6417            "APPROXIMATE_SIMILARITY" if f.args.len() == 1 => {
6418                let sig_sql = Self::expr_to_sql(&f.args[0]);
6419                let template = format!(
6420                    "SELECT CAST(SUM(CASE WHEN num_distinct = 1 THEN 1 ELSE 0 END) AS DOUBLE) / COUNT(*) FROM (SELECT pos, COUNT(DISTINCT h) AS num_distinct FROM (SELECT h, pos FROM UNNEST(LIST({sig})) AS _(sig) JOIN UNNEST(CAST(sig -> '$.state' AS UBIGINT[])) WITH ORDINALITY AS s(h, pos) ON TRUE) GROUP BY pos)",
6421                    sig = sig_sql
6422                );
6423                Self::parse_as_subquery(&template)
6424            }
6425
6426            // ARRAYS_ZIP(a1, a2, ...) -> struct list construction in DuckDB
6427            "ARRAYS_ZIP" if !f.args.is_empty() => {
6428                let args = f.args;
6429                let n = args.len();
6430                let is_null = |expr: Expression| {
6431                    Expression::IsNull(Box::new(crate::expressions::IsNull {
6432                        this: expr,
6433                        not: false,
6434                        postfix_form: false,
6435                    }))
6436                };
6437                let length_of = |expr: Expression| {
6438                    Expression::Function(Box::new(Function::new("LENGTH".to_string(), vec![expr])))
6439                };
6440                let eq_zero = |expr: Expression| {
6441                    Expression::Eq(Box::new(BinaryOp {
6442                        left: expr,
6443                        right: Expression::number(0),
6444                        left_comments: Vec::new(),
6445                        operator_comments: Vec::new(),
6446                        trailing_comments: Vec::new(),
6447                        inferred_type: None,
6448                    }))
6449                };
6450                let and_expr = |left: Expression, right: Expression| {
6451                    Expression::And(Box::new(BinaryOp {
6452                        left,
6453                        right,
6454                        left_comments: Vec::new(),
6455                        operator_comments: Vec::new(),
6456                        trailing_comments: Vec::new(),
6457                        inferred_type: None,
6458                    }))
6459                };
6460                let or_expr = |left: Expression, right: Expression| {
6461                    Expression::Or(Box::new(BinaryOp {
6462                        left,
6463                        right,
6464                        left_comments: Vec::new(),
6465                        operator_comments: Vec::new(),
6466                        trailing_comments: Vec::new(),
6467                        inferred_type: None,
6468                    }))
6469                };
6470
6471                let null_cond = args.iter().cloned().map(is_null).reduce(or_expr).unwrap();
6472                let empty_cond = args
6473                    .iter()
6474                    .cloned()
6475                    .map(|a| eq_zero(length_of(a)))
6476                    .reduce(and_expr)
6477                    .unwrap();
6478
6479                let null_struct = Expression::Struct(Box::new(Struct {
6480                    fields: (1..=n)
6481                        .map(|i| {
6482                            (
6483                                Some(format!("${}", i)),
6484                                Expression::Null(crate::expressions::Null),
6485                            )
6486                        })
6487                        .collect(),
6488                }));
6489                let empty_result = Expression::Array(Box::new(crate::expressions::Array {
6490                    expressions: vec![null_struct],
6491                    inferred_type: None,
6492                }));
6493
6494                let range_upper = if n == 1 {
6495                    length_of(args[0].clone())
6496                } else {
6497                    let length_null_cond = args
6498                        .iter()
6499                        .cloned()
6500                        .map(|a| is_null(length_of(a)))
6501                        .reduce(or_expr)
6502                        .unwrap();
6503                    let greatest_len = Expression::Greatest(Box::new(VarArgFunc {
6504                        expressions: args.iter().cloned().map(length_of).collect(),
6505                        original_name: None,
6506                        inferred_type: None,
6507                    }));
6508                    Expression::Case(Box::new(Case {
6509                        operand: None,
6510                        whens: vec![(length_null_cond, Expression::Null(crate::expressions::Null))],
6511                        else_: Some(greatest_len),
6512                        comments: Vec::new(),
6513                        inferred_type: None,
6514                    }))
6515                };
6516
6517                let range_expr = Expression::Function(Box::new(Function::new(
6518                    "RANGE".to_string(),
6519                    vec![Expression::number(0), range_upper],
6520                )));
6521
6522                let i_ident = Identifier::new("__i");
6523                let i_ref = Expression::boxed_column(Column {
6524                    table: None,
6525                    name: i_ident.clone(),
6526                    join_mark: false,
6527                    trailing_comments: Vec::new(),
6528                    span: None,
6529                    inferred_type: None,
6530                });
6531                let i_plus_one = Expression::Add(Box::new(BinaryOp {
6532                    left: i_ref,
6533                    right: Expression::number(1),
6534                    left_comments: Vec::new(),
6535                    operator_comments: Vec::new(),
6536                    trailing_comments: Vec::new(),
6537                    inferred_type: None,
6538                }));
6539                let empty_array = Expression::Array(Box::new(crate::expressions::Array {
6540                    expressions: vec![],
6541                    inferred_type: None,
6542                }));
6543                let zipped_struct = Expression::Struct(Box::new(Struct {
6544                    fields: args
6545                        .iter()
6546                        .enumerate()
6547                        .map(|(i, a)| {
6548                            let coalesced = Expression::Coalesce(Box::new(VarArgFunc {
6549                                expressions: vec![a.clone(), empty_array.clone()],
6550                                original_name: None,
6551                                inferred_type: None,
6552                            }));
6553                            let item =
6554                                Expression::Subscript(Box::new(crate::expressions::Subscript {
6555                                    this: coalesced,
6556                                    index: i_plus_one.clone(),
6557                                }));
6558                            (Some(format!("${}", i + 1)), item)
6559                        })
6560                        .collect(),
6561                }));
6562                let lambda_i = Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
6563                    parameters: vec![i_ident],
6564                    body: zipped_struct,
6565                    colon: false,
6566                    parameter_types: Vec::new(),
6567                }));
6568                let zipped_result = Expression::Function(Box::new(Function::new(
6569                    "LIST_TRANSFORM".to_string(),
6570                    vec![range_expr, lambda_i],
6571                )));
6572
6573                Ok(Expression::Case(Box::new(Case {
6574                    operand: None,
6575                    whens: vec![
6576                        (null_cond, Expression::Null(crate::expressions::Null)),
6577                        (empty_cond, empty_result),
6578                    ],
6579                    else_: Some(zipped_result),
6580                    comments: Vec::new(),
6581                    inferred_type: None,
6582                })))
6583            }
6584
6585            // STRTOK(str, delim, pos) -> complex CASE expression
6586            // Snowflake's STRTOK treats each character in delim as a separate delimiter,
6587            // so we use REGEXP_SPLIT_TO_ARRAY with a character class regex.
6588            "STRTOK" if f.args.len() == 3 => {
6589                let mut args = f.args.into_iter();
6590                let str_arg = args.next().unwrap();
6591                let delim_arg = args.next().unwrap();
6592                let pos_arg = args.next().unwrap();
6593
6594                // Helper: create empty string literal ''
6595                let empty_str = || Expression::string("".to_string());
6596                // Helper: create NULL literal
6597                let null_expr = || Expression::Null(crate::expressions::Null);
6598
6599                // WHEN delim = '' AND str = '' THEN NULL
6600                let when1_cond = Expression::And(Box::new(BinaryOp::new(
6601                    Expression::Eq(Box::new(BinaryOp::new(delim_arg.clone(), empty_str()))),
6602                    Expression::Eq(Box::new(BinaryOp::new(str_arg.clone(), empty_str()))),
6603                )));
6604
6605                // WHEN delim = '' AND pos = 1 THEN str
6606                let when2_cond = Expression::And(Box::new(BinaryOp::new(
6607                    Expression::Eq(Box::new(BinaryOp::new(delim_arg.clone(), empty_str()))),
6608                    Expression::Eq(Box::new(BinaryOp::new(
6609                        pos_arg.clone(),
6610                        Expression::number(1),
6611                    ))),
6612                )));
6613
6614                // WHEN delim = '' THEN NULL
6615                let when3_cond =
6616                    Expression::Eq(Box::new(BinaryOp::new(delim_arg.clone(), empty_str())));
6617
6618                // WHEN pos < 0 THEN NULL
6619                let when4_cond = Expression::Lt(Box::new(BinaryOp::new(
6620                    pos_arg.clone(),
6621                    Expression::number(0),
6622                )));
6623
6624                // WHEN str IS NULL OR delim IS NULL OR pos IS NULL THEN NULL
6625                let str_is_null = Expression::IsNull(Box::new(crate::expressions::IsNull {
6626                    this: str_arg.clone(),
6627                    not: false,
6628                    postfix_form: false,
6629                }));
6630                let delim_is_null = Expression::IsNull(Box::new(crate::expressions::IsNull {
6631                    this: delim_arg.clone(),
6632                    not: false,
6633                    postfix_form: false,
6634                }));
6635                let pos_is_null = Expression::IsNull(Box::new(crate::expressions::IsNull {
6636                    this: pos_arg.clone(),
6637                    not: false,
6638                    postfix_form: false,
6639                }));
6640                let when5_cond = Expression::Or(Box::new(BinaryOp::new(
6641                    Expression::Or(Box::new(BinaryOp::new(str_is_null, delim_is_null))),
6642                    pos_is_null,
6643                )));
6644
6645                // Inner CASE for the regex pattern:
6646                // CASE WHEN delim = '' THEN ''
6647                //      ELSE '[' || REGEXP_REPLACE(delim, '([\[\]^.\-*+?(){}|$\\])', '\\\1', 'g') || ']'
6648                // END
6649                let regex_replace = Expression::Function(Box::new(Function::new(
6650                    "REGEXP_REPLACE".to_string(),
6651                    vec![
6652                        delim_arg.clone(),
6653                        Expression::string(r"([\[\]^.\-*+?(){}|$\\])".to_string()),
6654                        Expression::string(r"\\\1".to_string()),
6655                        Expression::string("g".to_string()),
6656                    ],
6657                )));
6658
6659                // '[' || REGEXP_REPLACE(...) || ']'
6660                let concat_regex = Expression::DPipe(Box::new(crate::expressions::DPipe {
6661                    this: Box::new(Expression::DPipe(Box::new(crate::expressions::DPipe {
6662                        this: Box::new(Expression::string("[".to_string())),
6663                        expression: Box::new(regex_replace),
6664                        safe: None,
6665                    }))),
6666                    expression: Box::new(Expression::string("]".to_string())),
6667                    safe: None,
6668                }));
6669
6670                let inner_case = Expression::Case(Box::new(Case {
6671                    operand: None,
6672                    whens: vec![(
6673                        Expression::Eq(Box::new(BinaryOp::new(delim_arg.clone(), empty_str()))),
6674                        empty_str(),
6675                    )],
6676                    else_: Some(concat_regex),
6677                    comments: Vec::new(),
6678                    inferred_type: None,
6679                }));
6680
6681                // REGEXP_SPLIT_TO_ARRAY(str, <inner_case>)
6682                let regexp_split = Expression::Function(Box::new(Function::new(
6683                    "REGEXP_SPLIT_TO_ARRAY".to_string(),
6684                    vec![str_arg.clone(), inner_case],
6685                )));
6686
6687                // Lambda: x -> NOT x = ''
6688                let lambda = Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
6689                    parameters: vec![Identifier::new("x".to_string())],
6690                    body: Expression::Not(Box::new(crate::expressions::UnaryOp {
6691                        this: Expression::Eq(Box::new(BinaryOp::new(
6692                            Expression::boxed_column(Column {
6693                                table: None,
6694                                name: Identifier::new("x".to_string()),
6695                                join_mark: false,
6696                                trailing_comments: Vec::new(),
6697                                span: None,
6698                                inferred_type: None,
6699                            }),
6700                            empty_str(),
6701                        ))),
6702                        inferred_type: None,
6703                    })),
6704                    colon: false,
6705                    parameter_types: Vec::new(),
6706                }));
6707
6708                // LIST_FILTER(<regexp_split>, <lambda>)
6709                let list_filter = Expression::Function(Box::new(Function::new(
6710                    "LIST_FILTER".to_string(),
6711                    vec![regexp_split, lambda],
6712                )));
6713
6714                // LIST_FILTER(...)[pos]
6715                let subscripted = Expression::Subscript(Box::new(crate::expressions::Subscript {
6716                    this: list_filter,
6717                    index: pos_arg.clone(),
6718                }));
6719
6720                Ok(Expression::Case(Box::new(Case {
6721                    operand: None,
6722                    whens: vec![
6723                        (when1_cond, null_expr()),
6724                        (when2_cond, str_arg.clone()),
6725                        (when3_cond, null_expr()),
6726                        (when4_cond, null_expr()),
6727                        (when5_cond, null_expr()),
6728                    ],
6729                    else_: Some(subscripted),
6730                    comments: Vec::new(),
6731                    inferred_type: None,
6732                })))
6733            }
6734
6735            // Pass through everything else
6736            _ => Ok(Expression::Function(Box::new(f))),
6737        }
6738    }
6739
6740    /// Convert Snowflake date format to DuckDB strptime format
6741    fn convert_snowflake_date_format(&self, fmt: Expression) -> Expression {
6742        match fmt {
6743            Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
6744                let Literal::String(s) = lit.as_ref() else {
6745                    unreachable!()
6746                };
6747                let converted = Self::snowflake_to_strptime(&s);
6748                Expression::Literal(Box::new(Literal::String(converted)))
6749            }
6750            _ => fmt,
6751        }
6752    }
6753
6754    /// Convert Snowflake time format to DuckDB strptime format
6755    fn convert_snowflake_time_format(&self, fmt: Expression) -> Expression {
6756        match fmt {
6757            Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
6758                let Literal::String(s) = lit.as_ref() else {
6759                    unreachable!()
6760                };
6761                let converted = Self::snowflake_to_strptime(&s);
6762                Expression::Literal(Box::new(Literal::String(converted)))
6763            }
6764            _ => fmt,
6765        }
6766    }
6767
6768    /// Token-based conversion from Snowflake format strings (both original and normalized) to DuckDB strptime format.
6769    /// Handles both uppercase Snowflake originals (YYYY, MM, DD) and normalized lowercase forms (yyyy, mm, DD).
6770    fn snowflake_to_strptime(s: &str) -> String {
6771        let mut result = String::new();
6772        let mut cursor = crate::format_tokens::FormatTokenCursor::new(s);
6773        while let Some(ch) = cursor.peek() {
6774            // Compound patterns first
6775            if cursor.consume_prefix("HH24MISS", true) {
6776                result.push_str("%H%M%S");
6777            } else if cursor.consume_prefix("MMMM", true) {
6778                result.push_str("%B");
6779            } else if cursor.consume_prefix("YYYY", true) {
6780                result.push_str("%Y");
6781            } else if cursor.consume_prefix("YY", true) {
6782                result.push_str("%y");
6783            } else if cursor.consume_prefix("MON", true) {
6784                result.push_str("%b");
6785            } else if cursor.consume_prefix("HH24", true) {
6786                result.push_str("%H");
6787            } else if cursor.consume_prefix("HH12", true) {
6788                result.push_str("%I");
6789            } else if cursor.consume_prefix("HH", true) {
6790                result.push_str("%I");
6791            } else if cursor.consume_prefix("MISS", true) {
6792                result.push_str("%M%S");
6793            } else if cursor.consume_prefix("MI", true) {
6794                result.push_str("%M");
6795            } else if cursor.consume_prefix("MM", true) {
6796                result.push_str("%m");
6797            } else if cursor.consume_prefix("DD", true) {
6798                result.push_str("%d");
6799            } else if cursor.consume_prefix("DY", true) {
6800                result.push_str("%a");
6801            } else if cursor.consume_prefix("SS", true) {
6802                result.push_str("%S");
6803            } else if cursor.consume_prefix("FF", true) {
6804                // Preserve the existing one-digit precision rule: FF7-FF9 use
6805                // nanoseconds; FF0-FF6 and bare FF use microseconds.
6806                if let Some(digit) = cursor.peek().filter(|c| c.is_ascii_digit()) {
6807                    result.push_str(if digit >= '7' { "%n" } else { "%f" });
6808                    cursor.next_char();
6809                } else {
6810                    result.push_str("%f");
6811                }
6812            } else if cursor.consume_prefix("PM", true) || cursor.consume_prefix("AM", true) {
6813                result.push_str("%p");
6814            } else if cursor.consume_prefix("TZH", true) {
6815                result.push_str("%z");
6816            } else if cursor.consume_prefix("TZM", true) {
6817                // TZM is part of timezone, skip
6818            } else {
6819                result.push(ch);
6820                cursor.next_char();
6821            }
6822        }
6823        result
6824    }
6825
6826    /// Convert BigQuery format string to DuckDB strptime format
6827    /// BigQuery: %E6S -> DuckDB: %S.%f (seconds with microseconds)
6828    fn convert_bq_to_strptime_format(&self, fmt: Expression) -> Expression {
6829        match fmt {
6830            Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
6831                let Literal::String(s) = lit.as_ref() else {
6832                    unreachable!()
6833                };
6834                let converted = s.replace("%E6S", "%S.%f").replace("%E*S", "%S.%f");
6835                Expression::Literal(Box::new(Literal::String(converted)))
6836            }
6837            _ => fmt,
6838        }
6839    }
6840
6841    /// Transform DATE_PART(unit, expr) for DuckDB
6842    fn transform_date_part(&self, args: Vec<Expression>) -> Result<Expression> {
6843        let mut args = args;
6844        let unit_expr = args.remove(0);
6845        let date_expr = args.remove(0);
6846        let unit_name = match &unit_expr {
6847            Expression::Column(c) => c.name.name.to_uppercase(),
6848            Expression::Identifier(i) => i.name.to_uppercase(),
6849            Expression::Var(v) => v.this.to_uppercase(),
6850            Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
6851                let Literal::String(s) = lit.as_ref() else {
6852                    unreachable!()
6853                };
6854                s.to_uppercase()
6855            }
6856            _ => {
6857                return Ok(Expression::Function(Box::new(Function::new(
6858                    "DATE_PART".to_string(),
6859                    vec![unit_expr, date_expr],
6860                ))))
6861            }
6862        };
6863        match unit_name.as_str() {
6864            "EPOCH_SECOND" | "EPOCH" => Ok(Expression::Cast(Box::new(Cast {
6865                this: Expression::Function(Box::new(Function::new(
6866                    "EPOCH".to_string(),
6867                    vec![date_expr],
6868                ))),
6869                to: DataType::BigInt { length: None },
6870                trailing_comments: Vec::new(),
6871                double_colon_syntax: false,
6872                format: None,
6873                default: None,
6874                inferred_type: None,
6875            }))),
6876            "EPOCH_MILLISECOND" | "EPOCH_MILLISECONDS" => Ok(Expression::Function(Box::new(
6877                Function::new("EPOCH_MS".to_string(), vec![date_expr]),
6878            ))),
6879            "EPOCH_MICROSECOND" | "EPOCH_MICROSECONDS" => Ok(Expression::Function(Box::new(
6880                Function::new("EPOCH_US".to_string(), vec![date_expr]),
6881            ))),
6882            "EPOCH_NANOSECOND" | "EPOCH_NANOSECONDS" => Ok(Expression::Function(Box::new(
6883                Function::new("EPOCH_NS".to_string(), vec![date_expr]),
6884            ))),
6885            "DAYOFWEEKISO" | "DAYOFWEEK_ISO" => Ok(Expression::Extract(Box::new(
6886                crate::expressions::ExtractFunc {
6887                    this: date_expr,
6888                    field: crate::expressions::DateTimeField::Custom("ISODOW".to_string()),
6889                },
6890            ))),
6891            "YEAROFWEEK" | "YEAROFWEEKISO" => Ok(Expression::Cast(Box::new(Cast {
6892                this: Expression::Function(Box::new(Function::new(
6893                    "STRFTIME".to_string(),
6894                    vec![
6895                        date_expr,
6896                        Expression::Literal(Box::new(Literal::String("%G".to_string()))),
6897                    ],
6898                ))),
6899                to: DataType::Int {
6900                    length: None,
6901                    integer_spelling: false,
6902                },
6903                trailing_comments: Vec::new(),
6904                double_colon_syntax: false,
6905                format: None,
6906                default: None,
6907                inferred_type: None,
6908            }))),
6909            "WEEKISO" => Ok(Expression::Cast(Box::new(Cast {
6910                this: Expression::Function(Box::new(Function::new(
6911                    "STRFTIME".to_string(),
6912                    vec![
6913                        date_expr,
6914                        Expression::Literal(Box::new(Literal::String("%V".to_string()))),
6915                    ],
6916                ))),
6917                to: DataType::Int {
6918                    length: None,
6919                    integer_spelling: false,
6920                },
6921                trailing_comments: Vec::new(),
6922                double_colon_syntax: false,
6923                format: None,
6924                default: None,
6925                inferred_type: None,
6926            }))),
6927            "NANOSECOND" | "NANOSECONDS" | "NS" => Ok(Expression::Cast(Box::new(Cast {
6928                this: Expression::Function(Box::new(Function::new(
6929                    "STRFTIME".to_string(),
6930                    vec![
6931                        Expression::Cast(Box::new(Cast {
6932                            this: date_expr,
6933                            to: DataType::Custom {
6934                                name: "TIMESTAMP_NS".to_string(),
6935                            },
6936                            trailing_comments: Vec::new(),
6937                            double_colon_syntax: false,
6938                            format: None,
6939                            default: None,
6940                            inferred_type: None,
6941                        })),
6942                        Expression::Literal(Box::new(Literal::String("%n".to_string()))),
6943                    ],
6944                ))),
6945                to: DataType::BigInt { length: None },
6946                trailing_comments: Vec::new(),
6947                double_colon_syntax: false,
6948                format: None,
6949                default: None,
6950                inferred_type: None,
6951            }))),
6952            "DAYOFMONTH" => Ok(Expression::Extract(Box::new(
6953                crate::expressions::ExtractFunc {
6954                    this: date_expr,
6955                    field: crate::expressions::DateTimeField::Day,
6956                },
6957            ))),
6958            _ => {
6959                let field = match unit_name.as_str() {
6960                    "YEAR" | "YY" | "YYYY" => crate::expressions::DateTimeField::Year,
6961                    "MONTH" | "MON" | "MM" => crate::expressions::DateTimeField::Month,
6962                    "DAY" | "DD" | "D" => crate::expressions::DateTimeField::Day,
6963                    "HOUR" | "HH" => crate::expressions::DateTimeField::Hour,
6964                    "MINUTE" | "MI" | "MIN" => crate::expressions::DateTimeField::Minute,
6965                    "SECOND" | "SEC" | "SS" => crate::expressions::DateTimeField::Second,
6966                    "MILLISECOND" | "MS" => crate::expressions::DateTimeField::Millisecond,
6967                    "MICROSECOND" | "US" => crate::expressions::DateTimeField::Microsecond,
6968                    "QUARTER" | "QTR" => crate::expressions::DateTimeField::Quarter,
6969                    "WEEK" | "WK" => crate::expressions::DateTimeField::Week,
6970                    "DAYOFWEEK" | "DOW" => crate::expressions::DateTimeField::DayOfWeek,
6971                    "DAYOFYEAR" | "DOY" => crate::expressions::DateTimeField::DayOfYear,
6972                    "TIMEZONE_HOUR" => crate::expressions::DateTimeField::TimezoneHour,
6973                    "TIMEZONE_MINUTE" => crate::expressions::DateTimeField::TimezoneMinute,
6974                    _ => crate::expressions::DateTimeField::Custom(unit_name),
6975                };
6976                Ok(Expression::Extract(Box::new(
6977                    crate::expressions::ExtractFunc {
6978                        this: date_expr,
6979                        field,
6980                    },
6981                )))
6982            }
6983        }
6984    }
6985
6986    /// Transform DATEADD(unit, amount, date) for DuckDB
6987    fn transform_dateadd(&self, args: Vec<Expression>) -> Result<Expression> {
6988        let mut args = args;
6989        let unit_expr = args.remove(0);
6990        let amount = args.remove(0);
6991        let date = args.remove(0);
6992        let unit_name = match &unit_expr {
6993            Expression::Column(c) => c.name.name.to_uppercase(),
6994            Expression::Identifier(i) => i.name.to_uppercase(),
6995            Expression::Var(v) => v.this.to_uppercase(),
6996            Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
6997                let Literal::String(s) = lit.as_ref() else {
6998                    unreachable!()
6999                };
7000                s.to_uppercase()
7001            }
7002            _ => String::new(),
7003        };
7004        if unit_name == "NANOSECOND" || unit_name == "NS" {
7005            let epoch_ns = Expression::Function(Box::new(Function::new(
7006                "EPOCH_NS".to_string(),
7007                vec![Expression::Cast(Box::new(Cast {
7008                    this: date,
7009                    to: DataType::Custom {
7010                        name: "TIMESTAMP_NS".to_string(),
7011                    },
7012                    trailing_comments: Vec::new(),
7013                    double_colon_syntax: false,
7014                    format: None,
7015                    default: None,
7016                    inferred_type: None,
7017                }))],
7018            )));
7019            return Ok(Expression::Function(Box::new(Function::new(
7020                "MAKE_TIMESTAMP_NS".to_string(),
7021                vec![Expression::Add(Box::new(BinaryOp {
7022                    left: epoch_ns,
7023                    right: amount,
7024                    left_comments: Vec::new(),
7025                    operator_comments: Vec::new(),
7026                    trailing_comments: Vec::new(),
7027                    inferred_type: None,
7028                }))],
7029            ))));
7030        }
7031        let (interval_unit, multiplied_amount) = match unit_name.as_str() {
7032            "YEAR" | "YY" | "YYYY" => (IntervalUnit::Year, amount),
7033            "MONTH" | "MON" | "MM" => (IntervalUnit::Month, amount),
7034            "DAY" | "DD" | "D" => (IntervalUnit::Day, amount),
7035            "HOUR" | "HH" => (IntervalUnit::Hour, amount),
7036            "MINUTE" | "MI" | "MIN" => (IntervalUnit::Minute, amount),
7037            "SECOND" | "SEC" | "SS" => (IntervalUnit::Second, amount),
7038            "MILLISECOND" | "MS" => (IntervalUnit::Millisecond, amount),
7039            "MICROSECOND" | "US" => (IntervalUnit::Microsecond, amount),
7040            "WEEK" | "WK" => (
7041                IntervalUnit::Day,
7042                Expression::Mul(Box::new(BinaryOp {
7043                    left: amount,
7044                    right: Expression::number(7),
7045                    left_comments: Vec::new(),
7046                    operator_comments: Vec::new(),
7047                    trailing_comments: Vec::new(),
7048                    inferred_type: None,
7049                })),
7050            ),
7051            "QUARTER" | "QTR" => (
7052                IntervalUnit::Month,
7053                Expression::Mul(Box::new(BinaryOp {
7054                    left: amount,
7055                    right: Expression::number(3),
7056                    left_comments: Vec::new(),
7057                    operator_comments: Vec::new(),
7058                    trailing_comments: Vec::new(),
7059                    inferred_type: None,
7060                })),
7061            ),
7062            _ => (IntervalUnit::Day, amount),
7063        };
7064        Ok(Expression::Add(Box::new(BinaryOp {
7065            left: date,
7066            right: Expression::Interval(Box::new(Interval {
7067                this: Some(multiplied_amount),
7068                unit: Some(IntervalUnitSpec::Simple {
7069                    unit: interval_unit,
7070                    use_plural: false,
7071                }),
7072            })),
7073            left_comments: Vec::new(),
7074            operator_comments: Vec::new(),
7075            trailing_comments: Vec::new(),
7076            inferred_type: None,
7077        })))
7078    }
7079
7080    /// Transform DATEDIFF(unit, start, end) for DuckDB
7081    fn transform_datediff(&self, args: Vec<Expression>) -> Result<Expression> {
7082        let mut args = args;
7083        let unit_expr = args.remove(0);
7084        let start = args.remove(0);
7085        let end = args.remove(0);
7086        let unit_name = match &unit_expr {
7087            Expression::Column(c) => c.name.name.to_uppercase(),
7088            Expression::Identifier(i) => i.name.to_uppercase(),
7089            Expression::Var(v) => v.this.to_uppercase(),
7090            Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
7091                let Literal::String(s) = lit.as_ref() else {
7092                    unreachable!()
7093                };
7094                s.to_uppercase()
7095            }
7096            _ => String::new(),
7097        };
7098        if unit_name == "NANOSECOND" || unit_name == "NS" {
7099            let epoch_end = Expression::Function(Box::new(Function::new(
7100                "EPOCH_NS".to_string(),
7101                vec![Expression::Cast(Box::new(Cast {
7102                    this: end,
7103                    to: DataType::Custom {
7104                        name: "TIMESTAMP_NS".to_string(),
7105                    },
7106                    trailing_comments: Vec::new(),
7107                    double_colon_syntax: false,
7108                    format: None,
7109                    default: None,
7110                    inferred_type: None,
7111                }))],
7112            )));
7113            let epoch_start = Expression::Function(Box::new(Function::new(
7114                "EPOCH_NS".to_string(),
7115                vec![Expression::Cast(Box::new(Cast {
7116                    this: start,
7117                    to: DataType::Custom {
7118                        name: "TIMESTAMP_NS".to_string(),
7119                    },
7120                    trailing_comments: Vec::new(),
7121                    double_colon_syntax: false,
7122                    format: None,
7123                    default: None,
7124                    inferred_type: None,
7125                }))],
7126            )));
7127            return Ok(Expression::Sub(Box::new(BinaryOp {
7128                left: epoch_end,
7129                right: epoch_start,
7130                left_comments: Vec::new(),
7131                operator_comments: Vec::new(),
7132                trailing_comments: Vec::new(),
7133                inferred_type: None,
7134            })));
7135        }
7136        if unit_name == "WEEK" || unit_name == "WK" {
7137            let trunc_start = Expression::Function(Box::new(Function::new(
7138                "DATE_TRUNC".to_string(),
7139                vec![
7140                    Expression::Literal(Box::new(Literal::String("WEEK".to_string()))),
7141                    Expression::Cast(Box::new(Cast {
7142                        this: start,
7143                        to: DataType::Date,
7144                        trailing_comments: Vec::new(),
7145                        double_colon_syntax: false,
7146                        format: None,
7147                        default: None,
7148                        inferred_type: None,
7149                    })),
7150                ],
7151            )));
7152            let trunc_end = Expression::Function(Box::new(Function::new(
7153                "DATE_TRUNC".to_string(),
7154                vec![
7155                    Expression::Literal(Box::new(Literal::String("WEEK".to_string()))),
7156                    Expression::Cast(Box::new(Cast {
7157                        this: end,
7158                        to: DataType::Date,
7159                        trailing_comments: Vec::new(),
7160                        double_colon_syntax: false,
7161                        format: None,
7162                        default: None,
7163                        inferred_type: None,
7164                    })),
7165                ],
7166            )));
7167            return Ok(Expression::Function(Box::new(Function::new(
7168                "DATE_DIFF".to_string(),
7169                vec![
7170                    Expression::Literal(Box::new(Literal::String("WEEK".to_string()))),
7171                    trunc_start,
7172                    trunc_end,
7173                ],
7174            ))));
7175        }
7176        let cast_if_string = |e: Expression| -> Expression {
7177            match &e {
7178                Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
7179                    Expression::Cast(Box::new(Cast {
7180                        this: e,
7181                        to: DataType::Date,
7182                        trailing_comments: Vec::new(),
7183                        double_colon_syntax: false,
7184                        format: None,
7185                        default: None,
7186                        inferred_type: None,
7187                    }))
7188                }
7189                _ => e,
7190            }
7191        };
7192        let start = cast_if_string(start);
7193        let end = cast_if_string(end);
7194        Ok(Expression::Function(Box::new(Function::new(
7195            "DATE_DIFF".to_string(),
7196            vec![
7197                Expression::Literal(Box::new(Literal::String(unit_name))),
7198                start,
7199                end,
7200            ],
7201        ))))
7202    }
7203
7204    fn transform_aggregate_function(
7205        &self,
7206        f: Box<crate::expressions::AggregateFunction>,
7207    ) -> Result<Expression> {
7208        let name_upper = f.name.to_uppercase();
7209        match name_upper.as_str() {
7210            // Keep the established canonical transpilation for a plain DuckDB
7211            // LIST aggregate now that the parser represents it as an aggregate
7212            // node. Modifier-bearing LIST calls retain their aggregate shape so
7213            // DISTINCT, ORDER BY, FILTER, and LIMIT are not discarded.
7214            "LIST"
7215                if f.args.len() == 1
7216                    && !f.distinct
7217                    && f.filter.is_none()
7218                    && f.order_by.is_empty()
7219                    && f.limit.is_none()
7220                    && f.ignore_nulls.is_none() =>
7221            {
7222                Ok(Expression::Function(Box::new(Function::new(
7223                    "ARRAY_AGG".to_string(),
7224                    f.args,
7225                ))))
7226            }
7227
7228            // GROUP_CONCAT -> LISTAGG
7229            "GROUP_CONCAT" if !f.args.is_empty() => Ok(Expression::Function(Box::new(
7230                Function::new("LISTAGG".to_string(), f.args),
7231            ))),
7232
7233            // LISTAGG is native to DuckDB
7234            "LISTAGG" => Ok(Expression::AggregateFunction(f)),
7235
7236            // STRING_AGG -> LISTAGG
7237            "STRING_AGG" if !f.args.is_empty() => Ok(Expression::Function(Box::new(
7238                Function::new("LISTAGG".to_string(), f.args),
7239            ))),
7240
7241            // ARRAY_AGG -> list (or array_agg, both work)
7242            "ARRAY_AGG" if !f.args.is_empty() => Ok(Expression::Function(Box::new(Function::new(
7243                "list".to_string(),
7244                f.args,
7245            )))),
7246
7247            // LOGICAL_OR -> BOOL_OR with CAST to BOOLEAN
7248            "LOGICAL_OR" if !f.args.is_empty() => {
7249                let arg = f.args.into_iter().next().unwrap();
7250                Ok(Expression::Function(Box::new(Function::new(
7251                    "BOOL_OR".to_string(),
7252                    vec![Expression::Cast(Box::new(crate::expressions::Cast {
7253                        this: arg,
7254                        to: crate::expressions::DataType::Boolean,
7255                        trailing_comments: Vec::new(),
7256                        double_colon_syntax: false,
7257                        format: None,
7258                        default: None,
7259                        inferred_type: None,
7260                    }))],
7261                ))))
7262            }
7263
7264            // LOGICAL_AND -> BOOL_AND with CAST to BOOLEAN
7265            "LOGICAL_AND" if !f.args.is_empty() => {
7266                let arg = f.args.into_iter().next().unwrap();
7267                Ok(Expression::Function(Box::new(Function::new(
7268                    "BOOL_AND".to_string(),
7269                    vec![Expression::Cast(Box::new(crate::expressions::Cast {
7270                        this: arg,
7271                        to: crate::expressions::DataType::Boolean,
7272                        trailing_comments: Vec::new(),
7273                        double_colon_syntax: false,
7274                        format: None,
7275                        default: None,
7276                        inferred_type: None,
7277                    }))],
7278                ))))
7279            }
7280
7281            // SKEW -> SKEWNESS
7282            "SKEW" => Ok(Expression::Function(Box::new(Function::new(
7283                "SKEWNESS".to_string(),
7284                f.args,
7285            )))),
7286
7287            // REGR_VALX(y, x) -> CASE WHEN y IS NULL THEN CAST(NULL AS DOUBLE) ELSE x END
7288            "REGR_VALX" if f.args.len() == 2 => {
7289                let mut args = f.args;
7290                let y = args.remove(0);
7291                let x = args.remove(0);
7292                Ok(Expression::Case(Box::new(Case {
7293                    operand: None,
7294                    whens: vec![(
7295                        Expression::IsNull(Box::new(crate::expressions::IsNull {
7296                            this: y,
7297                            not: false,
7298                            postfix_form: false,
7299                        })),
7300                        Expression::Cast(Box::new(Cast {
7301                            this: Expression::Null(crate::expressions::Null),
7302                            to: DataType::Double {
7303                                precision: None,
7304                                scale: None,
7305                            },
7306                            trailing_comments: Vec::new(),
7307                            double_colon_syntax: false,
7308                            format: None,
7309                            default: None,
7310                            inferred_type: None,
7311                        })),
7312                    )],
7313                    else_: Some(x),
7314                    comments: Vec::new(),
7315                    inferred_type: None,
7316                })))
7317            }
7318
7319            // REGR_VALY(y, x) -> CASE WHEN x IS NULL THEN CAST(NULL AS DOUBLE) ELSE y END
7320            "REGR_VALY" if f.args.len() == 2 => {
7321                let mut args = f.args;
7322                let y = args.remove(0);
7323                let x = args.remove(0);
7324                Ok(Expression::Case(Box::new(Case {
7325                    operand: None,
7326                    whens: vec![(
7327                        Expression::IsNull(Box::new(crate::expressions::IsNull {
7328                            this: x,
7329                            not: false,
7330                            postfix_form: false,
7331                        })),
7332                        Expression::Cast(Box::new(Cast {
7333                            this: Expression::Null(crate::expressions::Null),
7334                            to: DataType::Double {
7335                                precision: None,
7336                                scale: None,
7337                            },
7338                            trailing_comments: Vec::new(),
7339                            double_colon_syntax: false,
7340                            format: None,
7341                            default: None,
7342                            inferred_type: None,
7343                        })),
7344                    )],
7345                    else_: Some(y),
7346                    comments: Vec::new(),
7347                    inferred_type: None,
7348                })))
7349            }
7350
7351            // BOOLAND_AGG -> BOOL_AND(CAST(arg AS BOOLEAN))
7352            "BOOLAND_AGG" if !f.args.is_empty() => {
7353                let arg = f.args.into_iter().next().unwrap();
7354                Ok(Expression::Function(Box::new(Function::new(
7355                    "BOOL_AND".to_string(),
7356                    vec![Expression::Cast(Box::new(Cast {
7357                        this: arg,
7358                        to: DataType::Boolean,
7359                        trailing_comments: Vec::new(),
7360                        double_colon_syntax: false,
7361                        format: None,
7362                        default: None,
7363                        inferred_type: None,
7364                    }))],
7365                ))))
7366            }
7367
7368            // BOOLOR_AGG -> BOOL_OR(CAST(arg AS BOOLEAN))
7369            "BOOLOR_AGG" if !f.args.is_empty() => {
7370                let arg = f.args.into_iter().next().unwrap();
7371                Ok(Expression::Function(Box::new(Function::new(
7372                    "BOOL_OR".to_string(),
7373                    vec![Expression::Cast(Box::new(Cast {
7374                        this: arg,
7375                        to: DataType::Boolean,
7376                        trailing_comments: Vec::new(),
7377                        double_colon_syntax: false,
7378                        format: None,
7379                        default: None,
7380                        inferred_type: None,
7381                    }))],
7382                ))))
7383            }
7384
7385            // BOOLXOR_AGG(c) -> COUNT_IF(CAST(c AS BOOLEAN)) = 1
7386            "BOOLXOR_AGG" if !f.args.is_empty() => {
7387                let arg = f.args.into_iter().next().unwrap();
7388                Ok(Expression::Eq(Box::new(BinaryOp {
7389                    left: Expression::Function(Box::new(Function::new(
7390                        "COUNT_IF".to_string(),
7391                        vec![Expression::Cast(Box::new(Cast {
7392                            this: arg,
7393                            to: DataType::Boolean,
7394                            trailing_comments: Vec::new(),
7395                            double_colon_syntax: false,
7396                            format: None,
7397                            default: None,
7398                            inferred_type: None,
7399                        }))],
7400                    ))),
7401                    right: Expression::number(1),
7402                    left_comments: Vec::new(),
7403                    operator_comments: Vec::new(),
7404                    trailing_comments: Vec::new(),
7405                    inferred_type: None,
7406                })))
7407            }
7408
7409            // MAX_BY -> ARG_MAX
7410            "MAX_BY" if f.args.len() == 2 => Ok(Expression::AggregateFunction(Box::new(
7411                crate::expressions::AggregateFunction {
7412                    name: "ARG_MAX".to_string(),
7413                    ..(*f)
7414                },
7415            ))),
7416
7417            // MIN_BY -> ARG_MIN
7418            "MIN_BY" if f.args.len() == 2 => Ok(Expression::AggregateFunction(Box::new(
7419                crate::expressions::AggregateFunction {
7420                    name: "ARG_MIN".to_string(),
7421                    ..(*f)
7422                },
7423            ))),
7424
7425            // CORR - pass through (DuckDB handles NaN natively)
7426            "CORR" if f.args.len() == 2 => Ok(Expression::AggregateFunction(f)),
7427
7428            // BITMAP_CONSTRUCT_AGG(v) -> complex DuckDB subquery emulation
7429            "BITMAP_CONSTRUCT_AGG" if f.args.len() == 1 => {
7430                let v_sql = Self::expr_to_sql(&f.args[0]);
7431
7432                let template = format!(
7433                    "SELECT CASE WHEN l IS NULL OR LENGTH(l) = 0 THEN NULL WHEN LENGTH(l) <> LENGTH(LIST_FILTER(l, __v -> __v BETWEEN 0 AND 32767)) THEN NULL WHEN LENGTH(l) < 5 THEN UNHEX(PRINTF('%04X', LENGTH(l)) || h || REPEAT('00', GREATEST(0, 4 - LENGTH(l)) * 2)) ELSE UNHEX('08000000000000000000' || h) END FROM (SELECT l, COALESCE(LIST_REDUCE(LIST_TRANSFORM(l, __x -> PRINTF('%02X%02X', CAST(__x AS INT) & 255, (CAST(__x AS INT) >> 8) & 255)), (__a, __b) -> __a || __b, ''), '') AS h FROM (SELECT LIST_SORT(LIST_DISTINCT(LIST({v}) FILTER(WHERE NOT {v} IS NULL))) AS l))",
7434                    v = v_sql
7435                );
7436
7437                Self::parse_as_subquery(&template)
7438            }
7439
7440            // Pass through everything else
7441            _ => Ok(Expression::AggregateFunction(f)),
7442        }
7443    }
7444
7445    /// Convert Presto/MySQL format string to DuckDB format string
7446    /// DuckDB uses strftime/strptime C-style format specifiers
7447    /// Key difference: %i (Presto minutes) -> %M (DuckDB minutes)
7448    fn convert_format_to_duckdb(expr: &Expression) -> Expression {
7449        if let Expression::Literal(lit) = expr {
7450            if let Literal::String(s) = lit.as_ref() {
7451                let duckdb_fmt = Self::presto_to_duckdb_format(s);
7452                Expression::Literal(Box::new(Literal::String(duckdb_fmt)))
7453            } else {
7454                expr.clone()
7455            }
7456        } else {
7457            expr.clone()
7458        }
7459    }
7460
7461    /// Convert Presto format specifiers to DuckDB strftime format
7462    fn presto_to_duckdb_format(fmt: &str) -> String {
7463        let mut result = String::new();
7464        let chars: Vec<char> = fmt.chars().collect();
7465        let mut i = 0;
7466        while i < chars.len() {
7467            if chars[i] == '%' && i + 1 < chars.len() {
7468                match chars[i + 1] {
7469                    'i' => {
7470                        // Presto %i (minutes) -> DuckDB %M (minutes)
7471                        result.push_str("%M");
7472                        i += 2;
7473                    }
7474                    'T' => {
7475                        // Presto %T (time shorthand %H:%M:%S)
7476                        result.push_str("%H:%M:%S");
7477                        i += 2;
7478                    }
7479                    'F' => {
7480                        // Presto %F (date shorthand %Y-%m-%d)
7481                        result.push_str("%Y-%m-%d");
7482                        i += 2;
7483                    }
7484                    _ => {
7485                        result.push('%');
7486                        result.push(chars[i + 1]);
7487                        i += 2;
7488                    }
7489                }
7490            } else {
7491                result.push(chars[i]);
7492                i += 1;
7493            }
7494        }
7495        result
7496    }
7497}
7498
7499#[cfg(test)]
7500mod tests {
7501    use super::*;
7502    use crate::dialects::Dialect;
7503    use crate::expressions::JoinKind;
7504
7505    #[test]
7506    fn test_date_time_format_conversion_preserves_unicode() {
7507        for literal in ["é", "年", "🦀", "e\u{301}", "ß", "ſ"] {
7508            assert_eq!(DuckDBDialect::snowflake_to_strptime(literal), literal);
7509            assert_eq!(
7510                DuckDBDialect::snowflake_to_strptime(&format!("{literal}YYYY{literal}mM{literal}")),
7511                format!("{literal}%Y{literal}%m{literal}")
7512            );
7513        }
7514        for (input, expected) in [
7515            ("", ""),
7516            (
7517                "HH24MISS MMMM YYYY YY MON HH24 HH12 HH MISS MI MM DD DY SS PM AM TZH TZM",
7518                "%H%M%S %B %Y %y %b %H %I %I %M%S %M %m %d %a %S %p %p %z ",
7519            ),
7520            ("FF FF0 FF1 FF6 FF7 FF9 FF69", "%f %f %f %f %n %n %f9"),
7521            ("éFF7年YYYY", "é%n年%Y"),
7522            ("\"å¹´\"YYYY %q", "\"å¹´\"%Y %q"),
7523        ] {
7524            assert_eq!(
7525                DuckDBDialect::snowflake_to_strptime(input),
7526                expected,
7527                "{input}"
7528            );
7529        }
7530    }
7531
7532    fn transpile_to_duckdb(sql: &str) -> String {
7533        transpile_to_duckdb_from(sql, DialectType::Generic)
7534    }
7535
7536    fn transpile_to_duckdb_from(sql: &str, read: DialectType) -> String {
7537        let dialect = Dialect::get(read);
7538        let result = dialect
7539            .transpile(sql, DialectType::DuckDB)
7540            .expect("Transpile failed");
7541        result[0].clone()
7542    }
7543
7544    #[test]
7545    fn test_athena_max_by_preserves_filter() {
7546        let sql = "SELECT max_by(value,seq) FILTER (WHERE keep) AS answer FROM (VALUES ('kept',1,true),('ignored',2,false)) t(value,seq,keep)";
7547        assert_eq!(
7548            transpile_to_duckdb_from(sql, DialectType::Athena),
7549            "SELECT ARG_MAX_NULL(value, seq) FILTER(WHERE keep) AS answer FROM (VALUES ('kept', 1, TRUE), ('ignored', 2, FALSE)) AS t(value, seq, keep)"
7550        );
7551
7552        for read in [DialectType::Athena, DialectType::Presto, DialectType::Trino] {
7553            for (source, target) in [("MAX_BY", "ARG_MAX"), ("MIN_BY", "ARG_MIN")] {
7554                for args in ["value, seq", "value, seq, 2"] {
7555                    let target = if args == "value, seq" {
7556                        format!("{target}_NULL")
7557                    } else {
7558                        target.to_string()
7559                    };
7560                    for suffix in ["", " OVER (PARTITION BY category)"] {
7561                        let sql =
7562                            format!("SELECT {source}({args}) FILTER (WHERE keep){suffix} FROM t");
7563                        assert_eq!(
7564                            transpile_to_duckdb_from(&sql, read),
7565                            format!("SELECT {target}({args}) FILTER(WHERE keep){suffix} FROM t"),
7566                            "{read:?}: {sql}"
7567                        );
7568                    }
7569                }
7570            }
7571        }
7572    }
7573
7574    #[test]
7575    fn test_athena_max_by_preserves_null_winner() {
7576        for read in [DialectType::Athena, DialectType::Presto, DialectType::Trino] {
7577            for (source, target) in [("MAX_BY", "ARG_MAX_NULL"), ("MIN_BY", "ARG_MIN_NULL")] {
7578                let sql = format!(
7579                    "SELECT {source}(value, seq) AS answer FROM (VALUES ('older', 1), (CAST(NULL AS VARCHAR), 2)) t(value, seq)"
7580                );
7581                assert_eq!(
7582                    transpile_to_duckdb_from(&sql, read),
7583                    format!("SELECT {target}(value, seq) AS answer FROM (VALUES ('older', 1), (CAST(NULL AS TEXT), 2)) AS t(value, seq)"),
7584                    "{read:?}: {sql}"
7585                );
7586                assert_eq!(
7587                    transpile_to_duckdb_from(
7588                        &format!("SELECT {source}(DISTINCT value, seq) FILTER (WHERE keep) FROM t"),
7589                        read,
7590                    ),
7591                    format!("SELECT {target}(DISTINCT value, seq) FILTER(WHERE keep) FROM t")
7592                );
7593            }
7594        }
7595
7596        // DuckDB's native aliases intentionally skip NULL values.
7597        for (source, target) in [("MAX_BY", "ARG_MAX"), ("MIN_BY", "ARG_MIN")] {
7598            assert_eq!(
7599                transpile_to_duckdb_from(
7600                    &format!("SELECT {source}(value, seq) FROM t"),
7601                    DialectType::DuckDB,
7602                ),
7603                format!("SELECT {target}(value, seq) FROM t")
7604            );
7605        }
7606    }
7607
7608    #[test]
7609    fn test_athena_regexp_extract_preserves_null_and_empty_matches() {
7610        for read in [DialectType::Athena, DialectType::Presto, DialectType::Trino] {
7611            for args in [
7612                "'no-match', '([0-9]+)', 1",
7613                "'abc', '(z*)a', 1",
7614                "'abc', '(z)?a', 1",
7615                "'abc', '(a)(b)(c)'",
7616                "'abc', '(a)(b)(c)', 2",
7617                "'12 34', '[0-9]+'",
7618                "'', '()'",
7619                "NULL, '([0-9]+)', 1",
7620                "'abc', NULL, 1",
7621                "value, pattern, group_index",
7622            ] {
7623                assert_eq!(
7624                    transpile_to_duckdb_from(
7625                        &format!("SELECT REGEXP_EXTRACT({args}) AS answer"),
7626                        read,
7627                    ),
7628                    format!("SELECT ARRAY_EXTRACT(REGEXP_EXTRACT_ALL({args}), 1) AS answer"),
7629                    "{read:?}: {args}"
7630                );
7631            }
7632        }
7633
7634        assert_eq!(
7635            transpile_to_duckdb_from(
7636                "SELECT REGEXP_EXTRACT('no-match', '([0-9]+)', 1)",
7637                DialectType::DuckDB,
7638            ),
7639            "SELECT REGEXP_EXTRACT('no-match', '([0-9]+)', 1)"
7640        );
7641    }
7642
7643    #[test]
7644    fn test_athena_regexp_replace_capture_references() {
7645        for read in [DialectType::Athena, DialectType::Presto, DialectType::Trino] {
7646            for (sql, expected) in [
7647                (
7648                    "SELECT regexp_replace('2026.09 2027.10', '([0-9]+)[.]([0-9]+)', '$1-$2') AS answer",
7649                    r"SELECT REGEXP_REPLACE('2026.09 2027.10', '([0-9]+)[.]([0-9]+)', '\1-\2', 'g') AS answer",
7650                ),
7651                (
7652                    "SELECT regexp_replace(value, pattern, '$0/$1/$2') FROM t",
7653                    r"SELECT REGEXP_REPLACE(value, pattern, '\0/\1/\2', 'g') FROM t",
7654                ),
7655                (
7656                    r"SELECT regexp_replace('aa', '(a)', '\$1')",
7657                    "SELECT REGEXP_REPLACE('aa', '(a)', '$1', 'g')",
7658                ),
7659                (
7660                    "SELECT regexp_replace('a1b2', '[0-9]')",
7661                    "SELECT REGEXP_REPLACE('a1b2', '[0-9]', '', 'g')",
7662                ),
7663                (
7664                    "SELECT regexp_replace('aa', 'a', 'x')",
7665                    "SELECT REGEXP_REPLACE('aa', 'a', 'x', 'g')",
7666                ),
7667            ] {
7668                assert_eq!(transpile_to_duckdb_from(sql, read), expected, "{read:?}: {sql}");
7669            }
7670        }
7671
7672        assert_eq!(
7673            transpile_to_duckdb_from(
7674                "SELECT REGEXP_REPLACE('aa', '(a)', '$1')",
7675                DialectType::DuckDB,
7676            ),
7677            "SELECT REGEXP_REPLACE('aa', '(a)', '$1')"
7678        );
7679    }
7680
7681    // Optional execution assertions complement the SQL assertions on machines
7682    // with DuckDB installed, without making it a dependency of the Rust suite.
7683    fn assert_duckdb_value(sql: &str, expected: &str) {
7684        let Ok(engine) = std::env::var("POLYGLOT_DUCKDB") else {
7685            return;
7686        };
7687        for timezone in ["UTC", "America/New_York"] {
7688            let query = format!("SET TimeZone = '{timezone}'; {sql}");
7689            let output = std::process::Command::new(&engine)
7690                .args([
7691                    "-init",
7692                    "/dev/null",
7693                    "-noheader",
7694                    "-list",
7695                    "-nullvalue",
7696                    "NULL",
7697                    ":memory:",
7698                    &query,
7699                ])
7700                .output()
7701                .expect("run POLYGLOT_DUCKDB");
7702            assert!(
7703                output.status.success(),
7704                "{query}: {}",
7705                String::from_utf8_lossy(&output.stderr)
7706            );
7707            assert_eq!(
7708                String::from_utf8(output.stdout).unwrap().trim_end(),
7709                expected,
7710                "{query}"
7711            );
7712        }
7713    }
7714
7715    #[test]
7716    fn test_bigquery_numeric_precision_to_duckdb_479() {
7717        for (sql, expected_sql, expected_value) in [
7718            (
7719                "SELECT CAST(1.005 AS NUMERIC(10, 2)) AS a",
7720                "SELECT CAST(1.005 AS DECIMAL(10, 2)) AS a",
7721                "1.01",
7722            ),
7723            (
7724                "SELECT CAST(0.1249 AS NUMERIC)",
7725                "SELECT CAST(0.1249 AS DECIMAL(38, 9))",
7726                "0.124900000",
7727            ),
7728            (
7729                "SELECT ROUND(CAST(0.1249 AS NUMERIC), 2)",
7730                "SELECT ROUND(CAST(0.1249 AS DECIMAL(38, 9)), 2)",
7731                "0.12",
7732            ),
7733            (
7734                "SELECT SAFE_CAST('0.1249' AS NUMERIC)",
7735                "SELECT TRY_CAST('0.1249' AS DECIMAL(38, 9))",
7736                "0.124900000",
7737            ),
7738            (
7739                "SELECT NUMERIC '0.000000001'",
7740                "SELECT CAST('0.000000001' AS DECIMAL(38, 9))",
7741                "0.000000001",
7742            ),
7743        ] {
7744            let output = transpile_to_duckdb_from(sql, DialectType::BigQuery);
7745            assert_eq!(output, expected_sql, "{sql}");
7746            assert_duckdb_value(&output, expected_value);
7747        }
7748        for (sql, expected) in [
7749            ("SELECT CAST(-1.005 AS DECIMAL(10, 2))", "-1.01"),
7750            ("SELECT CAST(1.6 AS NUMERIC(10))", "2"),
7751            ("SELECT ROUND(CAST(-0.1249 AS NUMERIC), 2)", "-0.12"),
7752            ("SELECT SAFE_CAST('invalid' AS NUMERIC)", "NULL"),
7753            (
7754                "SELECT CAST('12345678901234567890123456789.123456789' AS NUMERIC)",
7755                "12345678901234567890123456789.123456789",
7756            ),
7757            (
7758                "SELECT COALESCE(CAST(NULL AS NUMERIC), DECIMAL '0.1249')",
7759                "0.124900000",
7760            ),
7761            (
7762                "SELECT CAST(['0.1249'] AS ARRAY<NUMERIC>)[OFFSET(0)]",
7763                "0.124900000",
7764            ),
7765            ("SELECT ABS(CAST(-0.1249 AS NUMERIC))", "0.124900000"),
7766            (
7767                "SELECT ROUND(NUMERIC '2.25', 1, 'ROUND_HALF_AWAY_FROM_ZERO')",
7768                "2.3",
7769            ),
7770            ("SELECT ROUND(NUMERIC '2.25', 1, 'ROUND_HALF_EVEN')", "2.2"),
7771            (
7772                "SELECT ROUND(NUMERIC '0.1249', 2, 'ROUND_HALF_AWAY_FROM_ZERO')",
7773                "0.12",
7774            ),
7775            (
7776                "SELECT ROUND(NUMERIC '0.1349', 2, 'ROUND_HALF_EVEN')",
7777                "0.13",
7778            ),
7779        ] {
7780            let output = transpile_to_duckdb_from(sql, DialectType::BigQuery);
7781            assert_duckdb_value(&output, expected);
7782        }
7783        let ddl = transpile_to_duckdb_from(
7784            "CREATE TABLE t (a NUMERIC, b NUMERIC(10, 2), c ARRAY<NUMERIC>, d STRUCT<n NUMERIC>)",
7785            DialectType::BigQuery,
7786        );
7787        assert_duckdb_value(
7788            &format!("{ddl}; INSERT INTO t VALUES ('0.1249', 1.005, ['0.1249'], {{'n': '0.1249'}}); SELECT a, b, c[1], d.n FROM t"),
7789            "0.124900000|1.01|0.124900000|0.124900000",
7790        );
7791        assert_eq!(
7792            transpile_to_duckdb_from("SELECT CAST(0.1249 AS NUMERIC)", DialectType::DuckDB),
7793            "SELECT CAST(0.1249 AS DECIMAL(18, 3))"
7794        );
7795    }
7796
7797    #[test]
7798    fn test_bigquery_named_parameters_to_duckdb_478() {
7799        let unnest = transpile_to_duckdb_from(
7800            "SELECT * FROM UNNEST(@sites) AS site",
7801            DialectType::BigQuery,
7802        );
7803        assert_eq!(unnest, "SELECT * FROM UNNEST($sites) AS _t0(site)");
7804        assert_duckdb_value(
7805            &format!("PREPARE q AS {unnest}; EXECUTE q(sites := ['alpha', 'beta'])"),
7806            "alpha\nbeta",
7807        );
7808        let membership = transpile_to_duckdb_from(
7809            "SELECT x FROM t WHERE id IN UNNEST(@ids)",
7810            DialectType::BigQuery,
7811        );
7812        assert!(!membership.contains("ABS("), "{membership}");
7813        assert!(
7814            membership.contains("ARRAY_CONTAINS($ids, id)"),
7815            "{membership}"
7816        );
7817        for (ids, expected) in [
7818            ("[2]", "b"),
7819            ("[]", ""),
7820            ("NULL::INTEGER[]", ""),
7821            ("[NULL, 1]", "a"),
7822        ] {
7823            assert_duckdb_value(
7824                &format!("CREATE TABLE t AS SELECT 1 AS id, 'a' AS x UNION ALL SELECT 2, 'b'; PREPARE q AS {membership}; EXECUTE q(ids := {ids})"), expected,
7825            );
7826        }
7827        for (sql, expected_sql, binding, value) in [
7828            ("SELECT @name + 1", "SELECT $name + 1", "name := 6", "7"),
7829            ("SELECT @select", "SELECT $select", "\"select\" := 7", "7"),
7830            ("SELECT @`a b`", "SELECT $\"a b\"", "\"a b\" := 7", "7"),
7831            (
7832                "SELECT @`a\"b`",
7833                "SELECT $\"a\"\"b\"",
7834                "\"a\"\"b\" := 7",
7835                "7",
7836            ),
7837            (
7838                "SELECT @param.dataField",
7839                "SELECT $param.dataField",
7840                "param := {'dataField': 7}",
7841                "7",
7842            ),
7843            (
7844                "SELECT @ids[OFFSET(0)]",
7845                "SELECT $ids[1]",
7846                "ids := [7, 8]",
7847                "7",
7848            ),
7849        ] {
7850            let output = transpile_to_duckdb_from(sql, DialectType::BigQuery);
7851            assert_eq!(output, expected_sql, "{sql}");
7852            assert_eq!(
7853                transpile_to_duckdb_from(&output, DialectType::DuckDB),
7854                output
7855            );
7856            assert_duckdb_value(
7857                &format!("PREPARE q AS {output}; EXECUTE q({binding})"),
7858                value,
7859            );
7860        }
7861        for dialect in [
7862            DialectType::PostgreSQL,
7863            DialectType::DuckDB,
7864            DialectType::Vertica,
7865        ] {
7866            assert_eq!(
7867                transpile_to_duckdb_from("SELECT @(-7)", dialect),
7868                "SELECT ABS((-7))"
7869            );
7870            let output = transpile_to_duckdb_from("SELECT @n FROM (SELECT -7 AS n) AS t", dialect);
7871            assert!(output.contains("ABS(n)"), "{dialect:?}: {output}");
7872            assert_duckdb_value(&output, "7");
7873        }
7874        let reverse = Dialect::get(DialectType::DuckDB)
7875            .transpile("SELECT $\"a b\"", DialectType::BigQuery)
7876            .unwrap();
7877        assert_eq!(reverse[0], "SELECT @`a b`");
7878    }
7879
7880    #[test]
7881    fn test_bigquery_variable_interval_to_duckdb_477() {
7882        for (sql, expected_sql, value) in [
7883            (
7884                "SELECT DATE_ADD(DATE '2026-01-01', INTERVAL n MONTH) AS d FROM (SELECT 2 AS n)",
7885                "SELECT CAST('2026-01-01' AS DATE) + INTERVAL (n) MONTH AS d FROM (SELECT 2 AS n)",
7886                "2026-03-01 00:00:00",
7887            ),
7888            (
7889                "SELECT DATE_ADD(DATE '2026-01-01', INTERVAL DATE_DIFF(DATE '2026-03-15', DATE '2026-01-01', MONTH) MONTH) AS d",
7890                "SELECT CAST('2026-01-01' AS DATE) + INTERVAL (DATE_DIFF('MONTH', CAST('2026-01-01' AS DATE), CAST('2026-03-15' AS DATE))) MONTH AS d",
7891                "2026-03-01 00:00:00",
7892            ),
7893        ] {
7894            let output = transpile_to_duckdb_from(sql, DialectType::BigQuery);
7895            assert_eq!(output, expected_sql);
7896            assert_duckdb_value(&output, value);
7897        }
7898        for (amount, expected) in [
7899            ("(n)", "2026-03-01 00:00:00"),
7900            ("(n + 1)", "2026-04-01 00:00:00"),
7901            ("ABS(-n)", "2026-03-01 00:00:00"),
7902            ("CASE WHEN n > 0 THEN n ELSE 1 END", "2026-03-01 00:00:00"),
7903            ("-n", "2025-11-01 00:00:00"),
7904            ("NULL", "NULL"),
7905        ] {
7906            let sql = format!(
7907                "SELECT DATE_ADD(DATE '2026-01-01', INTERVAL {amount} MONTH) FROM (SELECT 2 AS n)"
7908            );
7909            let output = transpile_to_duckdb_from(&sql, DialectType::BigQuery);
7910            assert_duckdb_value(&output, expected);
7911        }
7912        for (sql, expected) in [
7913            ("SELECT INTERVAL (n) MONTH FROM (SELECT 2 AS n)", "2 months"),
7914            (
7915                "SELECT INTERVAL (ABS(n)) MONTH FROM (SELECT -2 AS n)",
7916                "2 months",
7917            ),
7918            (
7919                "SELECT INTERVAL (s) FROM (SELECT '1 month' AS s)",
7920                "1 month",
7921            ),
7922            ("SELECT INTERVAL '2' MONTH", "2 months"),
7923        ] {
7924            let output = transpile_to_duckdb_from(sql, DialectType::DuckDB);
7925            assert!(!output.contains("INTERVAL (("), "{output}");
7926            assert_duckdb_value(&output, expected);
7927        }
7928        let output = transpile_to_duckdb_from(
7929            "SELECT DATE_SUB(DATE '2026-01-01', INTERVAL DATE_DIFF(DATE '2026-03-15', DATE '2026-01-01', MONTH) MONTH)",
7930            DialectType::BigQuery,
7931        );
7932        assert_duckdb_value(&output, "2025-11-01 00:00:00");
7933    }
7934
7935    #[test]
7936    fn test_bigquery_date_timezone_to_duckdb_476() {
7937        let source = Dialect::get(DialectType::BigQuery);
7938        for (argument, zone, expected) in [
7939            (
7940                "TIMESTAMP '2026-01-31 23:30:00+00'",
7941                "'Europe/Berlin'",
7942                "2026-02-01",
7943            ),
7944            (
7945                "TIMESTAMP '2016-12-25 05:30:00+07'",
7946                "'America/Los_Angeles'",
7947                "2016-12-24",
7948            ),
7949            (
7950                "TIMESTAMP '2026-07-31 22:30:00+00'",
7951                "'Europe/Berlin'",
7952                "2026-08-01",
7953            ),
7954            (
7955                "TIMESTAMP '2026-01-31 23:30:00-05:00'",
7956                "'UTC'",
7957                "2026-02-01",
7958            ),
7959            (
7960                "TIMESTAMP '2026-01-31T23:30:00Z'",
7961                "'Europe/Berlin'",
7962                "2026-02-01",
7963            ),
7964            (
7965                "TIMESTAMP '2026-01-31 23:30:00 America/New_York'",
7966                "'UTC'",
7967                "2026-02-01",
7968            ),
7969            (
7970                "TIMESTAMP '2016-12-25'",
7971                "'America/Los_Angeles'",
7972                "2016-12-24",
7973            ),
7974            (
7975                "TIMESTAMP '2026-01-31 23:30:00'",
7976                "'Europe/Berlin'",
7977                "2026-02-01",
7978            ),
7979            ("'2026-01-31 23:30:00'", "'Europe/Berlin'", "2026-02-01"),
7980            (
7981                "TIMESTAMP('2026-01-31 23:30:00')",
7982                "'Europe/Berlin'",
7983                "2026-02-01",
7984            ),
7985            (
7986                "CAST('2026-01-31 23:30:00' AS TIMESTAMP)",
7987                "'Europe/Berlin'",
7988                "2026-02-01",
7989            ),
7990            (
7991                "COALESCE(NULL, TIMESTAMP '2026-01-31 23:30:00')",
7992                "'Europe/Berlin'",
7993                "2026-02-01",
7994            ),
7995            (
7996                "TIMESTAMP_MILLIS(1769902200000)",
7997                "'Europe/Berlin'",
7998                "2026-02-01",
7999            ),
8000            (
8001                "TIMESTAMP_MICROS(1769902200000000)",
8002                "'Europe/Berlin'",
8003                "2026-02-01",
8004            ),
8005            (
8006                "TIMESTAMP_SECONDS(1769902200)",
8007                "'Europe/Berlin'",
8008                "2026-02-01",
8009            ),
8010            (
8011                "TIMESTAMP '2026-01-31 20:00:00+00'",
8012                "'+05:30'",
8013                "2026-02-01",
8014            ),
8015            ("TIMESTAMP '2026-02-01 01:00:00+00'", "'-08'", "2026-01-31"),
8016            ("NULL", "'Europe/Berlin'", "NULL"),
8017            ("TIMESTAMP '2026-01-31 23:30:00+00'", "NULL", "NULL"),
8018        ] {
8019            let input = format!("SELECT DATE({argument}, {zone}) AS d");
8020            let output = source
8021                .transpile(&input, DialectType::DuckDB)
8022                .unwrap()
8023                .remove(0);
8024            assert!(!output.contains("STRPTIME"), "{input}: {output}");
8025            assert!(output.contains("AT TIME ZONE"), "{input}: {output}");
8026            assert_duckdb_value(&output, expected);
8027        }
8028        let input = "SELECT DATE(TIMESTAMP '2026-01-31 23:30:00+00', 'Europe/Berlin') AS d";
8029        assert_eq!(source.transpile(input, DialectType::DuckDB).unwrap()[0],
8030            "SELECT CAST(CAST('2026-01-31 23:30:00+00' AS TIMESTAMPTZ) AT TIME ZONE 'Europe/Berlin' AS DATE) AS d");
8031
8032        let input = "SELECT DATE(ts, zone) AS d FROM (SELECT TIMESTAMP '2026-01-31 23:30:00+00' AS ts, 'Europe/Berlin' AS zone) AS t";
8033        let output = source
8034            .transpile(input, DialectType::DuckDB)
8035            .unwrap()
8036            .remove(0);
8037        assert!(
8038            output.contains("CAST(ts AS TIMESTAMPTZ) AT TIME ZONE zone"),
8039            "{output}"
8040        );
8041        assert_duckdb_value(&output, "2026-02-01");
8042
8043        // Keep other DATE overloads and source dialect meanings intact.
8044        for (sql, read, expected) in [
8045            (
8046                "SELECT DATE(2026, 2, 1)",
8047                DialectType::BigQuery,
8048                "2026-02-01",
8049            ),
8050            (
8051                "SELECT DATE(DATETIME '2026-02-01 12:00:00')",
8052                DialectType::BigQuery,
8053                "2026-02-01",
8054            ),
8055            (
8056                "SELECT DATE('2026-02-01', 'YYYY-MM-DD')",
8057                DialectType::Snowflake,
8058                "2026-02-01",
8059            ),
8060        ] {
8061            let output = transpile_to_duckdb_from(sql, read);
8062            assert_duckdb_value(&output, expected);
8063        }
8064        let identity = source
8065            .transpile(
8066                "SELECT DATE(ts, 'Europe/Berlin') FROM t",
8067                DialectType::BigQuery,
8068            )
8069            .unwrap();
8070        assert_eq!(identity[0], "SELECT DATE(ts, 'Europe/Berlin') FROM t");
8071    }
8072
8073    #[test]
8074    fn test_athena_to_iso8601_exact_translation() {
8075        use crate::dialects::TranspileOptions;
8076        use crate::generator::UnsupportedLevel;
8077
8078        for read in [DialectType::Athena, DialectType::Trino] {
8079            for (argument, target_argument, format, expected_value) in [
8080                (
8081                    "DATE '2026-05-26'",
8082                    "CAST('2026-05-26' AS DATE)",
8083                    "%Y-%m-%d",
8084                    "2026-05-26",
8085                ),
8086                (
8087                    "TIMESTAMP '2026-05-26 02:08:02'",
8088                    "CAST('2026-05-26 02:08:02' AS TIMESTAMP(0))",
8089                    "%Y-%m-%dT%H:%M:%S",
8090                    "2026-05-26T02:08:02",
8091                ),
8092                (
8093                    "TIMESTAMP '2026-05-26 02:08:02.930'",
8094                    "CAST('2026-05-26 02:08:02.930' AS TIMESTAMP(3))",
8095                    "%Y-%m-%dT%H:%M:%S.%g",
8096                    "2026-05-26T02:08:02.930",
8097                ),
8098                (
8099                    "TIMESTAMP '2026-05-26 02:08:02.123456'",
8100                    "CAST('2026-05-26 02:08:02.123456' AS TIMESTAMP(6))",
8101                    "%Y-%m-%dT%H:%M:%S.%f",
8102                    "2026-05-26T02:08:02.123456",
8103                ),
8104                (
8105                    "TIMESTAMP '2026-05-26 02:08:02.123456789'",
8106                    "CAST('2026-05-26 02:08:02.123456789' AS TIMESTAMP(9))",
8107                    "%Y-%m-%dT%H:%M:%S.%n",
8108                    "2026-05-26T02:08:02.123456789",
8109                ),
8110                (
8111                    "TIMESTAMP '1969-12-31 23:59:59.123456789'",
8112                    "CAST('1969-12-31 23:59:59.123456789' AS TIMESTAMP(9))",
8113                    "%Y-%m-%dT%H:%M:%S.%n",
8114                    "1969-12-31T23:59:59.123456789",
8115                ),
8116                (
8117                    "CAST('2026-05-26 02:08:02.930' AS TIMESTAMP)",
8118                    "CAST('2026-05-26 02:08:02.930' AS TIMESTAMP(3))",
8119                    "%Y-%m-%dT%H:%M:%S.%g",
8120                    "2026-05-26T02:08:02.930",
8121                ),
8122                (
8123                    "CAST(TIMESTAMP '2026-05-26 02:08:02.930' AS TIMESTAMP(6))",
8124                    "CAST(CAST('2026-05-26 02:08:02.930' AS TIMESTAMP(3)) AS TIMESTAMP(6))",
8125                    "%Y-%m-%dT%H:%M:%S.%f",
8126                    "2026-05-26T02:08:02.930000",
8127                ),
8128                (
8129                    "CAST(CASE WHEN TRUE THEN TIMESTAMP '2026-05-26 02:08:02.123456789' ELSE NULL END AS TIMESTAMP(9))",
8130                    "CAST(CASE WHEN TRUE THEN CAST('2026-05-26 02:08:02.123456789' AS TIMESTAMP(9)) ELSE NULL END AS TIMESTAMP(9))",
8131                    "%Y-%m-%dT%H:%M:%S.%n",
8132                    "2026-05-26T02:08:02.123456789",
8133                ),
8134                (
8135                    "(TIMESTAMP '2026-05-26 02:08:02.930')",
8136                    "(CAST('2026-05-26 02:08:02.930' AS TIMESTAMP(3)))",
8137                    "%Y-%m-%dT%H:%M:%S.%g",
8138                    "2026-05-26T02:08:02.930",
8139                ),
8140                (
8141                    "CAST(NULL AS DATE)",
8142                    "CAST(NULL AS DATE)",
8143                    "%Y-%m-%d",
8144                    "NULL",
8145                ),
8146                (
8147                    "CAST(NULL AS TIMESTAMP(3))",
8148                    "CAST(NULL AS TIMESTAMP(3))",
8149                    "%Y-%m-%dT%H:%M:%S.%g",
8150                    "NULL",
8151                ),
8152                (
8153                    "TRY_CAST('invalid' AS TIMESTAMP(3))",
8154                    "TRY_CAST('invalid' AS TIMESTAMP(3))",
8155                    "%Y-%m-%dT%H:%M:%S.%g",
8156                    "NULL",
8157                ),
8158            ] {
8159                let sql = format!("SELECT TO_ISO8601({argument}) AS answer");
8160                for level in [
8161                    UnsupportedLevel::Ignore,
8162                    UnsupportedLevel::Warn,
8163                    UnsupportedLevel::Raise,
8164                    UnsupportedLevel::Immediate,
8165                ] {
8166                    let output = Dialect::get(read)
8167                        .transpile_with(
8168                            &sql,
8169                            DialectType::DuckDB,
8170                            TranspileOptions {
8171                                unsupported_level: level,
8172                                ..Default::default()
8173                            },
8174                        )
8175                        .unwrap();
8176                    assert_eq!(
8177                        output[0],
8178                        format!("SELECT STRFTIME({target_argument}, '{format}') AS answer"),
8179                        "{read:?}/{level:?}: {sql}"
8180                    );
8181                    if level == UnsupportedLevel::Ignore {
8182                        assert_duckdb_value(&output[0], expected_value);
8183                    }
8184                }
8185            }
8186        }
8187        // DATE has no session-dependent timestamp behavior in Presto either.
8188        let output = Dialect::get(DialectType::Presto)
8189            .transpile_with(
8190                "SELECT TO_ISO8601(DATE '2026-05-26')",
8191                DialectType::DuckDB,
8192                TranspileOptions::strict(),
8193            )
8194            .unwrap();
8195        assert_duckdb_value(&output[0], "2026-05-26");
8196        assert_eq!(
8197            output[0],
8198            "SELECT STRFTIME(CAST('2026-05-26' AS DATE), '%Y-%m-%d')"
8199        );
8200    }
8201
8202    #[test]
8203    fn test_athena_to_iso8601_permissive_translation() {
8204        use crate::dialects::TranspileOptions;
8205        use crate::generator::UnsupportedLevel;
8206
8207        for read in [DialectType::Athena, DialectType::Presto, DialectType::Trino] {
8208            let mut cases = vec![
8209                ("TIMESTAMP '2026-05-26 02:08:02.93'", "CAST('2026-05-26 02:08:02.93' AS TIMESTAMP(3))", false, "2026-05-26T02:08:02.930000000"),
8210                ("TIMESTAMP '2026-05-26 02:08:02.930 +02:00'", "CAST(CAST('2026-05-26 02:08:02.930+02:00' AS TIMESTAMPTZ) AT TIME ZONE 'UTC' AS TIMESTAMP(3))", true, "2026-05-26T00:08:02.930000000Z"),
8211                ("TIMESTAMP '2026-05-26 02:08:02.930 Z'", "CAST(CAST('2026-05-26 02:08:02.930Z' AS TIMESTAMPTZ) AT TIME ZONE 'UTC' AS TIMESTAMP(3))", true, "2026-05-26T02:08:02.930000000Z"),
8212                ("TIMESTAMP '2026-05-26 02:08:02.930 America/New_York'", "CAST(CAST('2026-05-26 02:08:02.930 America/New_York' AS TIMESTAMPTZ) AT TIME ZONE 'UTC' AS TIMESTAMP(3))", true, "2026-05-26T06:08:02.930000000Z"),
8213                ("CAST(NULL AS TIMESTAMP WITH TIME ZONE)", "CAST(CAST(NULL AS TIMESTAMPTZ) AT TIME ZONE 'UTC' AS TIMESTAMP(3))", true, "NULL"),
8214                ("TRY_CAST('invalid' AS TIMESTAMP WITH TIME ZONE)", "CAST(TRY_CAST('invalid' AS TIMESTAMPTZ) AT TIME ZONE 'UTC' AS TIMESTAMP(3))", true, "NULL"),
8215            ];
8216            if read == DialectType::Presto {
8217                cases.push((
8218                    "TIMESTAMP '2026-05-26 02:08:02.930'",
8219                    "CAST('2026-05-26 02:08:02.930' AS TIMESTAMP(3))",
8220                    false,
8221                    "2026-05-26T02:08:02.930000000",
8222                ));
8223            } else {
8224                cases.extend([
8225                    ("TIMESTAMP '2026-05-26 02:08:02.1234567'", "CAST('2026-05-26 02:08:02.1234567' AS TIMESTAMP(9))", false, "2026-05-26T02:08:02.123456700"),
8226                    ("TIMESTAMP '1969-12-31 23:59:59.123456 -02:00'", "CAST(CAST('1969-12-31 23:59:59.123456-02:00' AS TIMESTAMPTZ) AT TIME ZONE 'UTC' AS TIMESTAMP(6))", true, "1970-01-01T01:59:59.123456000Z"),
8227                ]);
8228            }
8229            for (argument, target_argument, timezone, expected_value) in cases {
8230                let sql = format!("SELECT TO_ISO8601({argument}) AS answer");
8231                for level in [
8232                    UnsupportedLevel::Ignore,
8233                    UnsupportedLevel::Warn,
8234                    UnsupportedLevel::Raise,
8235                    UnsupportedLevel::Immediate,
8236                ] {
8237                    let result = Dialect::get(read).transpile_with(
8238                        &sql,
8239                        DialectType::DuckDB,
8240                        TranspileOptions {
8241                            unsupported_level: level,
8242                            ..Default::default()
8243                        },
8244                    );
8245                    if matches!(level, UnsupportedLevel::Raise | UnsupportedLevel::Immediate) {
8246                        assert!(
8247                            matches!(result, Err(crate::error::Error::Unsupported { .. })),
8248                            "{read:?}/{level:?}: {sql}: {result:?}"
8249                        );
8250                    } else {
8251                        let output = result.unwrap();
8252                        let suffix = if timezone { " || 'Z'" } else { "" };
8253                        assert_eq!(output[0], format!("SELECT STRFTIME({target_argument}, '%Y-%m-%dT%H:%M:%S.%n'){suffix} AS answer"), "{read:?}/{level:?}: {sql}");
8254                        if level == UnsupportedLevel::Ignore {
8255                            assert_duckdb_value(&output[0], expected_value);
8256                        }
8257                    }
8258                }
8259            }
8260        }
8261    }
8262
8263    #[test]
8264    fn test_athena_to_iso8601_rejects_unverified_conversions() {
8265        use crate::dialects::TranspileOptions;
8266        use crate::generator::UnsupportedLevel;
8267
8268        for read in [DialectType::Athena, DialectType::Presto, DialectType::Trino] {
8269            let dialect = Dialect::get(read);
8270            for argument in [
8271                "value",
8272                "NULL",
8273                "'2026-05-26'",
8274                "42",
8275                "CAST(value AS VARCHAR)",
8276                "TIMESTAMP '2026-05-26 02:08:02.123456789123'",
8277                "TIMESTAMP '2026-05-26 02:08:02.123456789 +02:00'",
8278                "CAST(value AS TIMESTAMP(2))",
8279                "CAST(value AS TIMESTAMP(12))",
8280                "CAST(TIMESTAMP '2026-05-26 02:08:02.123456789' AS TIMESTAMP(6))",
8281                "CAST('2026-05-26 02:08:02.930999' AS TIMESTAMP(3))",
8282                "CAST(TIMESTAMP '2026-05-26 00:08:02.930 +02:00' AS DATE)",
8283                "CAST(TIMESTAMP '2026-05-26 00:08:02.930 +02:00' AS TIMESTAMP(3))",
8284                "DISTINCT DATE '2026-05-26'",
8285                "DATE '2026-05-26', DATE '2026-05-27'",
8286                "",
8287            ] {
8288                let sql = format!("SELECT TO_ISO8601({argument}) AS answer FROM t");
8289                for level in [
8290                    UnsupportedLevel::Ignore,
8291                    UnsupportedLevel::Warn,
8292                    UnsupportedLevel::Raise,
8293                    UnsupportedLevel::Immediate,
8294                ] {
8295                    let error = dialect
8296                        .transpile_with(
8297                            &sql,
8298                            DialectType::DuckDB,
8299                            TranspileOptions {
8300                                unsupported_level: level,
8301                                ..Default::default()
8302                            },
8303                        )
8304                        .expect_err("unsupported TO_ISO8601 must not emit invalid DuckDB SQL");
8305                    assert!(
8306                        matches!(error, crate::error::Error::Unsupported { .. }),
8307                        "{read:?}/{level:?}: {sql}: {error}"
8308                    );
8309                    assert!(error.to_string().contains("TO_ISO8601"));
8310                }
8311            }
8312
8313            assert_eq!(
8314                dialect
8315                    .transpile("SELECT TO_ISO8601(value) FROM t", read)
8316                    .unwrap()[0],
8317                "SELECT TO_ISO8601(value) FROM t"
8318            );
8319            assert_eq!(
8320                dialect
8321                    .transpile_with(
8322                        "SELECT custom.TO_ISO8601(value) FROM t",
8323                        DialectType::DuckDB,
8324                        TranspileOptions::strict()
8325                    )
8326                    .unwrap()[0],
8327                "SELECT custom.TO_ISO8601(value) FROM t"
8328            );
8329        }
8330    }
8331
8332    #[test]
8333    fn test_athena_to_iso8601_annotated_argument() {
8334        for (data_type, format) in [
8335            (DataType::Date, "%Y-%m-%d"),
8336            (
8337                DataType::Timestamp {
8338                    precision: Some(6),
8339                    timezone: false,
8340                },
8341                "%Y-%m-%dT%H:%M:%S.%f",
8342            ),
8343        ] {
8344            let mut argument = Expression::column("value");
8345            argument.set_inferred_type(data_type);
8346            let expression = Expression::Function(Box::new(Function::new(
8347                "TO_ISO8601".to_string(),
8348                vec![argument],
8349            )));
8350            let normalized = crate::dialects::normalization::normalize(
8351                expression,
8352                DialectType::Trino,
8353                DialectType::DuckDB,
8354                true,
8355            )
8356            .unwrap();
8357            assert_eq!(
8358                Dialect::get(DialectType::DuckDB)
8359                    .generate(&normalized)
8360                    .unwrap(),
8361                format!("STRFTIME(value, '{format}')")
8362            );
8363        }
8364        let mut argument = Expression::column("value");
8365        argument.set_inferred_type(DataType::Timestamp {
8366            precision: None,
8367            timezone: false,
8368        });
8369        for strict in [false, true] {
8370            let expression = Expression::Function(Box::new(Function::new(
8371                "TO_ISO8601".to_string(),
8372                vec![argument.clone()],
8373            )));
8374            assert!(crate::dialects::normalization::normalize(
8375                expression,
8376                DialectType::Trino,
8377                DialectType::DuckDB,
8378                strict
8379            )
8380            .is_err());
8381        }
8382    }
8383
8384    #[test]
8385    fn test_athena_to_iso8601_evaluates_argument_once() {
8386        let output = Dialect::get(DialectType::Athena).transpile_with(
8387            "SELECT TO_ISO8601(CAST(CASE WHEN NEXTVAL('calls') = 1 THEN '2026-05-26 02:08:02.930' ELSE '2000-01-01 00:00:00.000' END AS TIMESTAMP(3)))",
8388            DialectType::DuckDB,
8389            crate::dialects::TranspileOptions::strict(),
8390        ).unwrap();
8391        assert_eq!(output[0].matches("NEXTVAL").count(), 1);
8392        assert_duckdb_value(
8393            &format!(
8394                "CREATE SEQUENCE calls; {}; SELECT CURRVAL('calls');",
8395                output[0]
8396            ),
8397            "2026-05-26T02:08:02.930\n1",
8398        );
8399    }
8400
8401    #[test]
8402    fn test_ifnull_to_coalesce() {
8403        let result = transpile_to_duckdb("SELECT IFNULL(a, b)");
8404        assert!(
8405            result.contains("COALESCE"),
8406            "Expected COALESCE, got: {}",
8407            result
8408        );
8409    }
8410
8411    #[test]
8412    fn test_nvl_to_coalesce() {
8413        let result = transpile_to_duckdb("SELECT NVL(a, b)");
8414        assert!(
8415            result.contains("COALESCE"),
8416            "Expected COALESCE, got: {}",
8417            result
8418        );
8419    }
8420
8421    #[test]
8422    fn test_basic_select() {
8423        let result = transpile_to_duckdb("SELECT a, b FROM users WHERE id = 1");
8424        assert!(result.contains("SELECT"));
8425        assert!(result.contains("FROM users"));
8426    }
8427
8428    #[test]
8429    fn test_duckdb_keyword_relation_aliases() {
8430        let dialect = Dialect::get(DialectType::DuckDB);
8431        for alias in [
8432            "top",
8433            "TOP",
8434            "ToP",
8435            "first",
8436            "last",
8437            "begin",
8438            "type",
8439            "\"top\"",
8440            "\"where\"",
8441        ] {
8442            for template in [
8443                "SELECT {alias}.item_id FROM ranked_items {as}{alias}",
8444                "WITH ranked_items AS (SELECT 1 AS item_id) SELECT {alias}.item_id FROM ranked_items {as}{alias}",
8445                "WITH ranked_items AS (SELECT 1 AS item_id), selected_items AS (SELECT 1 AS item_id) SELECT {alias}.item_id FROM selected_items JOIN ranked_items {as}{alias} ON {alias}.item_id = selected_items.item_id",
8446                "SELECT {alias}.item_id FROM (SELECT 1 AS item_id) {as}{alias}",
8447                "SELECT {alias}.item_id FROM range(1) {as}{alias}(item_id)",
8448                "SELECT {alias}.item_id FROM (VALUES (1)) {as}{alias}(item_id)",
8449            ] {
8450                let template = template.replace("{alias}", alias);
8451                let explicit_sql = template.replace("{as}", "AS ");
8452                let expected = transpile_to_duckdb_from(&explicit_sql, DialectType::DuckDB);
8453                for as_keyword in ["", "AS "] {
8454                    let sql = template.replace("{as}", as_keyword);
8455                    let statements = dialect
8456                        .parse(&sql)
8457                        .unwrap_or_else(|error| panic!("Failed to parse {sql:?}: {error}"));
8458                    assert_eq!(statements.len(), 1, "{sql}");
8459                    let Expression::Select(select) = &statements[0] else {
8460                        panic!("Expected SELECT for {sql}");
8461                    };
8462                    let relation = select.joins.last().map_or_else(
8463                        || &select.from.as_ref().unwrap().expressions[0],
8464                        |join| &join.this,
8465                    );
8466                    let (identifier, columns) = match relation {
8467                        Expression::Table(table) => (table.alias.as_ref().unwrap(), &table.column_aliases),
8468                        Expression::Subquery(subquery) => (subquery.alias.as_ref().unwrap(), &subquery.column_aliases),
8469                        Expression::Alias(alias) => (&alias.alias, &alias.column_aliases),
8470                        _ => panic!("Expected aliased relation for {sql}"),
8471                    };
8472                    assert_eq!(identifier.name, alias.trim_matches('"'), "{sql}");
8473                    assert_eq!(identifier.quoted, alias.starts_with('"'), "{sql}");
8474                    if template.contains("(item_id)") {
8475                        assert_eq!(columns.len(), 1, "{sql}");
8476                        assert_eq!(columns[0].name, "item_id", "{sql}");
8477                    }
8478                    assert_eq!(
8479                        transpile_to_duckdb_from(&sql, DialectType::DuckDB),
8480                        expected,
8481                        "{sql}"
8482                    );
8483                }
8484            }
8485        }
8486    }
8487
8488    #[test]
8489    fn test_duckdb_keyword_relation_aliases_preserve_clause_boundaries() {
8490        let dialect = Dialect::get(DialectType::DuckDB);
8491        for alias in ["", " top"] {
8492            let sql = format!(
8493                "SELECT item_id, ROW_NUMBER() OVER w AS rn FROM ranked_items{alias} \
8494                 WHERE item_id > 0 GROUP BY item_id HAVING COUNT(*) > 0 \
8495                 WINDOW w AS (ORDER BY item_id) QUALIFY rn = 1 \
8496                 ORDER BY item_id NULLS FIRST, rn NULLS LAST LIMIT 5 OFFSET 1"
8497            );
8498            let statements = dialect.parse(&sql).unwrap();
8499            let Expression::Select(select) = &statements[0] else {
8500                panic!("Expected SELECT for {sql}");
8501            };
8502            assert!(select.where_clause.is_some());
8503            assert!(select.group_by.is_some());
8504            assert!(select.having.is_some());
8505            assert!(select.windows.is_some());
8506            assert!(select.qualify.is_some());
8507            let ordering = &select.order_by.as_ref().unwrap().expressions;
8508            assert_eq!(ordering.len(), 2);
8509            assert_eq!(ordering[0].nulls_first, Some(true));
8510            assert_eq!(ordering[1].nulls_first, Some(false));
8511            assert!(select.limit.is_some());
8512            assert!(select.offset.is_some());
8513            let Expression::Table(table) = &select.from.as_ref().unwrap().expressions[0] else {
8514                panic!("Expected table source for {sql}");
8515            };
8516            assert_eq!(
8517                table.alias.as_ref().map(|a| a.name.as_str()),
8518                if alias.is_empty() { None } else { Some("top") }
8519            );
8520
8521            for (join, kind) in [
8522                ("LEFT JOIN selected_items ON TRUE", JoinKind::Left),
8523                ("POSITIONAL JOIN selected_items", JoinKind::Positional),
8524            ] {
8525                let sql = format!("SELECT * FROM ranked_items{alias} {join}");
8526                let statements = dialect.parse(&sql).unwrap();
8527                let Expression::Select(select) = &statements[0] else {
8528                    panic!("Expected SELECT for {sql}");
8529                };
8530                assert_eq!(select.joins.len(), 1, "{sql}");
8531                assert_eq!(select.joins[0].kind, kind, "{sql}");
8532                let Expression::Table(table) = &select.from.as_ref().unwrap().expressions[0] else {
8533                    panic!("Expected table source for {sql}");
8534                };
8535                assert_eq!(
8536                    table.alias.as_ref().map(|a| a.name.as_str()),
8537                    if alias.is_empty() { None } else { Some("top") }
8538                );
8539            }
8540        }
8541
8542        // Truly reserved DuckDB words must not become implicit relation aliases.
8543        for alias in ["end", "where", "qualify", "select", "join", "limit"] {
8544            let sql = format!("SELECT * FROM ranked_items {alias}");
8545            assert!(dialect.parse(&sql).is_err(), "Unexpectedly accepted {sql}");
8546        }
8547    }
8548
8549    #[test]
8550    fn test_extract_quoted_date_parts() {
8551        for (quoted, canonical) in [("year", "YEAR"), ("month", "MONTH"), ("day", "DAY")] {
8552            let sql = format!("SELECT EXTRACT('{quoted}' FROM created_at) FROM events");
8553            assert_eq!(
8554                transpile_to_duckdb_from(&sql, DialectType::DuckDB),
8555                format!("SELECT EXTRACT({canonical} FROM created_at) FROM events")
8556            );
8557        }
8558
8559        let unquoted = "SELECT EXTRACT(YEAR FROM created_at) FROM events";
8560        assert_eq!(
8561            transpile_to_duckdb_from(unquoted, DialectType::DuckDB),
8562            unquoted
8563        );
8564    }
8565
8566    #[test]
8567    fn test_group_concat_to_listagg() {
8568        let result = transpile_to_duckdb("SELECT GROUP_CONCAT(name)");
8569        assert!(
8570            result.contains("LISTAGG"),
8571            "Expected LISTAGG, got: {}",
8572            result
8573        );
8574    }
8575
8576    #[test]
8577    fn test_listagg_preserved() {
8578        let result = transpile_to_duckdb("SELECT LISTAGG(name)");
8579        assert!(
8580            result.contains("LISTAGG"),
8581            "Expected LISTAGG, got: {}",
8582            result
8583        );
8584    }
8585
8586    #[test]
8587    fn test_ordered_string_agg_uses_duckdb_listagg_order_syntax() {
8588        let result = transpile_to_duckdb_from(
8589            "SELECT string_agg(nm, ',' ORDER BY id) AS v FROM t",
8590            DialectType::PostgreSQL,
8591        );
8592        assert_eq!(result, "SELECT LISTAGG(nm, ',' ORDER BY id) AS v FROM t");
8593    }
8594
8595    #[test]
8596    fn test_ordered_listagg_uses_duckdb_order_syntax() {
8597        let result = transpile_to_duckdb_from(
8598            "SELECT LISTAGG(col, '|SEPARATOR|') WITHIN GROUP (ORDER BY col2) FROM t",
8599            DialectType::Snowflake,
8600        );
8601        assert_eq!(
8602            result,
8603            "SELECT LISTAGG(col, '|SEPARATOR|' ORDER BY col2) FROM t"
8604        );
8605    }
8606
8607    #[test]
8608    fn test_ordered_group_concat_uses_duckdb_listagg_order_syntax() {
8609        let result = transpile_to_duckdb_from(
8610            "SELECT GROUP_CONCAT(nm ORDER BY id SEPARATOR ',') AS v FROM t",
8611            DialectType::MySQL,
8612        );
8613        assert_eq!(result, "SELECT LISTAGG(nm, ',' ORDER BY id) AS v FROM t");
8614    }
8615
8616    #[test]
8617    fn test_date_format_to_strftime() {
8618        let result = transpile_to_duckdb("SELECT DATE_FORMAT(d, '%Y-%m-%d')");
8619        // Generator uppercases function names
8620        assert!(
8621            result.to_uppercase().contains("STRFTIME"),
8622            "Expected STRFTIME, got: {}",
8623            result
8624        );
8625    }
8626
8627    #[test]
8628    fn test_regexp_like_to_regexp_matches() {
8629        let result = transpile_to_duckdb("SELECT REGEXP_LIKE(name, 'pattern')");
8630        // Generator uppercases function names
8631        assert!(
8632            result.to_uppercase().contains("REGEXP_MATCHES"),
8633            "Expected REGEXP_MATCHES, got: {}",
8634            result
8635        );
8636    }
8637
8638    #[test]
8639    fn test_double_quote_identifiers() {
8640        // DuckDB uses double quotes for identifiers
8641        let dialect = Dialect::get(DialectType::DuckDB);
8642        let config = dialect.generator_config();
8643        assert_eq!(config.identifier_quote, '"');
8644    }
8645
8646    /// Helper for DuckDB identity tests (parse with DuckDB, generate with DuckDB)
8647    fn duckdb_identity(sql: &str) -> String {
8648        let dialect = Dialect::get(DialectType::DuckDB);
8649        let ast = dialect.parse(sql).expect("Parse failed");
8650        let transformed = dialect.transform(ast[0].clone()).expect("Transform failed");
8651        dialect.generate(&transformed).expect("Generate failed")
8652    }
8653
8654    #[test]
8655    fn test_interval_quoting() {
8656        // Test 137: INTERVAL value should be quoted for DuckDB
8657        let result = duckdb_identity("SELECT DATE_ADD(CAST('2020-01-01' AS DATE), INTERVAL 1 DAY)");
8658        assert_eq!(
8659            result, "SELECT CAST('2020-01-01' AS DATE) + INTERVAL '1' DAY",
8660            "Interval value should be quoted as string"
8661        );
8662    }
8663
8664    #[test]
8665    fn test_struct_pack_to_curly_brace() {
8666        // Test 221: STRUCT_PACK should become curly brace notation
8667        let result = duckdb_identity("CAST([STRUCT_PACK(a := 1)] AS STRUCT(a BIGINT)[])");
8668        assert_eq!(
8669            result, "CAST([{'a': 1}] AS STRUCT(a BIGINT)[])",
8670            "STRUCT_PACK should be transformed to curly brace notation"
8671        );
8672    }
8673
8674    #[test]
8675    fn test_struct_pack_nested() {
8676        // Test 220: Nested STRUCT_PACK
8677        let result = duckdb_identity("CAST([[STRUCT_PACK(a := 1)]] AS STRUCT(a BIGINT)[][])");
8678        assert_eq!(
8679            result, "CAST([[{'a': 1}]] AS STRUCT(a BIGINT)[][])",
8680            "Nested STRUCT_PACK should be transformed"
8681        );
8682    }
8683
8684    #[test]
8685    fn test_struct_pack_cast() {
8686        // Test 222: STRUCT_PACK with :: cast
8687        let result = duckdb_identity("STRUCT_PACK(a := 'b')::json");
8688        assert_eq!(
8689            result, "CAST({'a': 'b'} AS JSON)",
8690            "STRUCT_PACK with cast should be transformed"
8691        );
8692    }
8693
8694    #[test]
8695    fn test_list_value_to_bracket() {
8696        // Test 309: LIST_VALUE should become bracket notation
8697        let result = duckdb_identity("SELECT LIST_VALUE(1)[i]");
8698        assert_eq!(
8699            result, "SELECT [1][i]",
8700            "LIST_VALUE should be transformed to bracket notation"
8701        );
8702    }
8703
8704    #[test]
8705    fn test_list_value_in_struct_literal() {
8706        // Test 310: LIST_VALUE inside struct literal
8707        let result = duckdb_identity("{'x': LIST_VALUE(1)[i]}");
8708        assert_eq!(
8709            result, "{'x': [1][i]}",
8710            "LIST_VALUE inside struct literal should be transformed"
8711        );
8712    }
8713
8714    #[test]
8715    fn test_struct_pack_simple() {
8716        // Simple STRUCT_PACK without nesting
8717        let result = duckdb_identity("SELECT STRUCT_PACK(a := 1)");
8718        eprintln!("STRUCT_PACK result: {}", result);
8719        assert!(
8720            result.contains("{"),
8721            "Expected curly brace, got: {}",
8722            result
8723        );
8724    }
8725
8726    #[test]
8727    fn test_not_in_position() {
8728        // Test 78: NOT IN should become NOT (...) IN (...)
8729        // DuckDB prefers `NOT (expr) IN (list)` over `expr NOT IN (list)`
8730        let result = duckdb_identity(
8731            "SELECT col FROM t WHERE JSON_EXTRACT_STRING(col, '$.id') NOT IN ('b')",
8732        );
8733        assert_eq!(
8734            result, "SELECT col FROM t WHERE NOT (col ->> '$.id') IN ('b')",
8735            "NOT IN should have NOT moved outside and JSON expression wrapped"
8736        );
8737    }
8738
8739    #[test]
8740    fn test_unnest_comma_join_to_join_on_true() {
8741        // Test 310: Comma-join with UNNEST should become JOIN ... ON TRUE
8742        let result = duckdb_identity(
8743            "WITH _data AS (SELECT [{'a': 1, 'b': 2}, {'a': 2, 'b': 3}] AS col) SELECT t.col['b'] FROM _data, UNNEST(_data.col) AS t(col) WHERE t.col['a'] = 1",
8744        );
8745        assert_eq!(
8746            result,
8747            "WITH _data AS (SELECT [{'a': 1, 'b': 2}, {'a': 2, 'b': 3}] AS col) SELECT t.col['b'] FROM _data JOIN UNNEST(_data.col) AS t(col) ON TRUE WHERE t.col['a'] = 1",
8748            "Comma-join with UNNEST should become JOIN ON TRUE"
8749        );
8750    }
8751}