Skip to main content

polyglot_sql/dialects/
snowflake.rs

1//! Snowflake Dialect
2//!
3//! Snowflake-specific transformations based on sqlglot patterns.
4//! Key differences:
5//! - TRY_ prefix for safe operations (TRY_CAST, TRY_TO_NUMBER)
6//! - FLATTEN for unnesting arrays
7//! - QUALIFY clause support
8//! - ARRAY_CONSTRUCT, OBJECT_CONSTRUCT for arrays/objects
9//! - Variant type handling
10//! - Default case-insensitive identifiers (unquoted)
11
12use super::{DialectImpl, DialectType};
13use crate::error::Result;
14use crate::expressions::{
15    AggFunc, BinaryOp, Cast, CeilFunc, DataType, Expression, Function, IntervalUnit, ListAggFunc,
16    Literal, UnaryFunc, VarArgFunc,
17};
18use crate::generator::GeneratorConfig;
19use crate::tokens::TokenizerConfig;
20
21/// Convert IntervalUnit to string for Snowflake syntax
22fn interval_unit_to_str(unit: &IntervalUnit) -> String {
23    match unit {
24        IntervalUnit::Year => "YEAR".to_string(),
25        IntervalUnit::Quarter => "QUARTER".to_string(),
26        IntervalUnit::Month => "MONTH".to_string(),
27        IntervalUnit::Week => "WEEK".to_string(),
28        IntervalUnit::Day => "DAY".to_string(),
29        IntervalUnit::Hour => "HOUR".to_string(),
30        IntervalUnit::Minute => "MINUTE".to_string(),
31        IntervalUnit::Second => "SECOND".to_string(),
32        IntervalUnit::Millisecond => "MILLISECOND".to_string(),
33        IntervalUnit::Microsecond => "MICROSECOND".to_string(),
34        IntervalUnit::Nanosecond => "NANOSECOND".to_string(),
35    }
36}
37
38/// Snowflake dialect
39pub struct SnowflakeDialect;
40
41impl DialectImpl for SnowflakeDialect {
42    fn dialect_type(&self) -> DialectType {
43        DialectType::Snowflake
44    }
45
46    fn tokenizer_config(&self) -> TokenizerConfig {
47        let mut config = TokenizerConfig::default();
48        // Snowflake uses double quotes for identifiers
49        config.identifiers.insert('"', '"');
50        // Snowflake supports $$ string literals
51        config.quotes.insert("$$".to_string(), "$$".to_string());
52        // Snowflake does NOT support nested comments (per Python sqlglot)
53        config.nested_comments = false;
54        // Snowflake supports // as single-line comments (in addition to --)
55        config.comments.insert("//".to_string(), None);
56        config
57    }
58
59    fn generator_config(&self) -> GeneratorConfig {
60        use crate::generator::IdentifierQuoteStyle;
61        GeneratorConfig {
62            identifier_quote: '"',
63            identifier_quote_style: IdentifierQuoteStyle::DOUBLE_QUOTE,
64            dialect: Some(DialectType::Snowflake),
65            // Snowflake-specific settings from Python sqlglot
66            parameter_token: "$",
67            matched_by_source: false,
68            single_string_interval: true,
69            join_hints: false,
70            table_hints: false,
71            query_hints: false,
72            aggregate_filter_supported: false,
73            supports_table_copy: false,
74            collate_is_func: true,
75            limit_only_literals: true,
76            json_key_value_pair_sep: ",",
77            insert_overwrite: " OVERWRITE INTO",
78            struct_delimiter: ("(", ")"),
79            copy_params_are_wrapped: false,
80            copy_params_eq_required: true,
81            star_except: "EXCLUDE",
82            supports_exploding_projections: false,
83            array_concat_is_var_len: false,
84            supports_convert_timezone: true,
85            except_intersect_support_all_clause: false,
86            supports_median: true,
87            array_size_name: "ARRAY_SIZE",
88            supports_decode_case: true,
89            is_bool_allowed: false,
90            // Snowflake supports TRY_ prefix operations
91            try_supported: true,
92            // Snowflake supports NVL2
93            nvl2_supported: true,
94            // Snowflake uses FLATTEN for unnest
95            unnest_with_ordinality: false,
96            // Snowflake uses space before paren: ALL (subquery)
97            quantified_no_paren_space: false,
98            // Snowflake uses bracket-only array syntax: [1, 2, 3]
99            array_bracket_only: true,
100            ..Default::default()
101        }
102    }
103
104    fn transform_expr(&self, expr: Expression) -> Result<Expression> {
105        match expr {
106            // ===== Data Type Mappings =====
107            Expression::DataType(dt) => self.transform_data_type(dt),
108
109            // ===== NOT IN transformation =====
110            // Snowflake treats `value NOT IN (subquery)` as `VALUE <> ALL (subquery)`
111            // See: https://docs.snowflake.com/en/sql-reference/functions/in
112            Expression::In(in_expr) if in_expr.not && in_expr.query.is_some() => {
113                // Transform NOT IN (subquery) -> <> ALL (subquery)
114                let inner = in_expr.query.unwrap();
115                // Wrap in Subquery so generator outputs ALL (subquery) with space
116                let subquery = Expression::Subquery(Box::new(crate::expressions::Subquery {
117                    this: inner,
118                    alias: None,
119                    column_aliases: Vec::new(),
120                    order_by: None,
121                    limit: None,
122                    offset: None,
123                    distribute_by: None,
124                    sort_by: None,
125                    cluster_by: None,
126                    lateral: false,
127                    modifiers_inside: false,
128                    trailing_comments: Vec::new(),
129                    inferred_type: None,
130                }));
131                Ok(Expression::All(Box::new(
132                    crate::expressions::QuantifiedExpr {
133                        this: in_expr.this,
134                        subquery,
135                        op: Some(crate::expressions::QuantifiedOp::Neq),
136                    },
137                )))
138            }
139
140            // NOT IN (values) -> NOT x IN (values)
141            Expression::In(in_expr) if in_expr.not => {
142                // Transform NOT x IN (values) by wrapping the In expression with not=false inside a Not
143                let in_without_not = crate::expressions::In {
144                    this: in_expr.this,
145                    expressions: in_expr.expressions,
146                    query: in_expr.query,
147                    not: false,
148                    global: in_expr.global,
149                    unnest: in_expr.unnest,
150                    is_field: in_expr.is_field,
151                };
152                Ok(Expression::Not(Box::new(crate::expressions::UnaryOp {
153                    this: Expression::In(Box::new(in_without_not)),
154                    inferred_type: None,
155                })))
156            }
157
158            // ===== Interval unit expansion =====
159            // Expand abbreviated units in interval string values (e.g., '1 w' -> '1 WEEK')
160            Expression::Interval(interval) => self.transform_interval(*interval),
161
162            // ===== Null handling =====
163            // IFNULL -> COALESCE (both work in Snowflake, but COALESCE is standard)
164            Expression::IfNull(f) => Ok(Expression::Coalesce(Box::new(VarArgFunc {
165                original_name: None,
166                expressions: vec![f.this, f.expression],
167                inferred_type: None,
168            }))),
169
170            // NVL -> COALESCE (both work in Snowflake, but COALESCE is standard)
171            Expression::Nvl(f) => Ok(Expression::Coalesce(Box::new(VarArgFunc {
172                original_name: None,
173                expressions: vec![f.this, f.expression],
174                inferred_type: None,
175            }))),
176
177            // Coalesce with original_name (e.g., IFNULL parsed as Coalesce) -> clear original_name
178            Expression::Coalesce(mut f) => {
179                f.original_name = None;
180                Ok(Expression::Coalesce(f))
181            }
182
183            // GROUP_CONCAT -> LISTAGG in Snowflake
184            Expression::GroupConcat(f) => Ok(Expression::ListAgg(Box::new(ListAggFunc {
185                this: f.this,
186                separator: f.separator,
187                on_overflow: None,
188                order_by: f.order_by,
189                distinct: f.distinct,
190                filter: f.filter,
191                inferred_type: None,
192            }))),
193
194            // ===== Cast operations =====
195            // CAST(x AS GEOGRAPHY) -> TO_GEOGRAPHY(x)
196            // CAST(x AS GEOMETRY) -> TO_GEOMETRY(x)
197            Expression::Cast(c) => {
198                use crate::expressions::DataType;
199                // First, recursively transform the inner expression
200                let transformed_this = self.transform_expr(c.this)?;
201                match &c.to {
202                    DataType::Geography { .. } => Ok(Expression::Function(Box::new(
203                        Function::new("TO_GEOGRAPHY".to_string(), vec![transformed_this]),
204                    ))),
205                    DataType::Geometry { .. } => Ok(Expression::Function(Box::new(Function::new(
206                        "TO_GEOMETRY".to_string(),
207                        vec![transformed_this],
208                    )))),
209                    _ => {
210                        // Transform the data type
211                        let transformed_dt = match self.transform_data_type(c.to.clone())? {
212                            Expression::DataType(dt) => dt,
213                            _ => c.to.clone(),
214                        };
215                        Ok(Expression::Cast(Box::new(Cast {
216                            this: transformed_this,
217                            to: transformed_dt,
218                            double_colon_syntax: false, // Normalize :: to CAST()
219                            trailing_comments: c.trailing_comments,
220                            format: c.format,
221                            default: c.default,
222                            inferred_type: None,
223                        })))
224                    }
225                }
226            }
227
228            // TryCast stays as TryCast (Snowflake supports TRY_CAST)
229            // Recursively transform the inner expression
230            Expression::TryCast(c) => {
231                let transformed_this = self.transform_expr(c.this)?;
232                Ok(Expression::TryCast(Box::new(Cast {
233                    this: transformed_this,
234                    to: c.to,
235                    double_colon_syntax: false, // Normalize :: to CAST()
236                    trailing_comments: c.trailing_comments,
237                    format: c.format,
238                    default: c.default,
239                    inferred_type: None,
240                })))
241            }
242
243            // SafeCast -> Cast in Snowflake (Snowflake CAST is safe by default)
244            // Also convert TIMESTAMP to TIMESTAMPTZ (BigQuery TIMESTAMP = tz-aware)
245            Expression::SafeCast(c) => {
246                let to = match c.to {
247                    DataType::Timestamp { .. } => DataType::Custom {
248                        name: "TIMESTAMPTZ".to_string(),
249                    },
250                    DataType::Custom { name } if name.eq_ignore_ascii_case("TIMESTAMP") => {
251                        DataType::Custom {
252                            name: "TIMESTAMPTZ".to_string(),
253                        }
254                    }
255                    other => other,
256                };
257                let transformed_this = self.transform_expr(c.this)?;
258                Ok(Expression::Cast(Box::new(Cast {
259                    this: transformed_this,
260                    to,
261                    double_colon_syntax: c.double_colon_syntax,
262                    trailing_comments: c.trailing_comments,
263                    format: c.format,
264                    default: c.default,
265                    inferred_type: None,
266                })))
267            }
268
269            // ===== Typed Literals -> CAST =====
270            // TIMESTAMP '...' -> CAST('...' AS TIMESTAMP)
271            Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Timestamp(_)) => {
272                let Literal::Timestamp(s) = lit.as_ref() else {
273                    unreachable!()
274                };
275                Ok(Expression::Cast(Box::new(Cast {
276                    this: Expression::Literal(Box::new(Literal::String(s.clone()))),
277                    to: DataType::Timestamp {
278                        precision: None,
279                        timezone: false,
280                    },
281                    double_colon_syntax: false,
282                    trailing_comments: Vec::new(),
283                    format: None,
284                    default: None,
285                    inferred_type: None,
286                })))
287            }
288
289            // DATE '...' -> CAST('...' AS DATE)
290            Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Date(_)) => {
291                let Literal::Date(s) = lit.as_ref() else {
292                    unreachable!()
293                };
294                Ok(Expression::Cast(Box::new(Cast {
295                    this: Expression::Literal(Box::new(Literal::String(s.clone()))),
296                    to: DataType::Date,
297                    double_colon_syntax: false,
298                    trailing_comments: Vec::new(),
299                    format: None,
300                    default: None,
301                    inferred_type: None,
302                })))
303            }
304
305            // TIME '...' -> CAST('...' AS TIME)
306            Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Time(_)) => {
307                let Literal::Time(s) = lit.as_ref() else {
308                    unreachable!()
309                };
310                Ok(Expression::Cast(Box::new(Cast {
311                    this: Expression::Literal(Box::new(Literal::String(s.clone()))),
312                    to: DataType::Time {
313                        precision: None,
314                        timezone: false,
315                    },
316                    double_colon_syntax: false,
317                    trailing_comments: Vec::new(),
318                    format: None,
319                    default: None,
320                    inferred_type: None,
321                })))
322            }
323
324            // DATETIME '...' -> CAST('...' AS DATETIME)
325            Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Datetime(_)) => {
326                let Literal::Datetime(s) = lit.as_ref() else {
327                    unreachable!()
328                };
329                Ok(Expression::Cast(Box::new(Cast {
330                    this: Expression::Literal(Box::new(Literal::String(s.clone()))),
331                    to: DataType::Custom {
332                        name: "DATETIME".to_string(),
333                    },
334                    double_colon_syntax: false,
335                    trailing_comments: Vec::new(),
336                    format: None,
337                    default: None,
338                    inferred_type: None,
339                })))
340            }
341
342            // ===== Pattern matching =====
343            // ILIKE is native to Snowflake (no transformation needed)
344            Expression::ILike(op) => Ok(Expression::ILike(op)),
345
346            // ===== Array operations =====
347            // EXPLODE -> FLATTEN in Snowflake
348            Expression::Explode(f) => Ok(Expression::Function(Box::new(Function::new(
349                "FLATTEN".to_string(),
350                vec![f.this],
351            )))),
352
353            // ExplodeOuter -> FLATTEN with OUTER => TRUE
354            Expression::ExplodeOuter(f) => Ok(Expression::Function(Box::new(Function::new(
355                "FLATTEN".to_string(),
356                vec![f.this],
357            )))),
358
359            // UNNEST -> TABLE(FLATTEN(INPUT => x)) AS _t0(seq, key, path, index, value, this)
360            Expression::Unnest(f) => {
361                // Create INPUT => x named argument
362                let input_arg =
363                    Expression::NamedArgument(Box::new(crate::expressions::NamedArgument {
364                        name: crate::expressions::Identifier::new("INPUT"),
365                        value: f.this,
366                        separator: crate::expressions::NamedArgSeparator::DArrow,
367                    }));
368
369                // Create FLATTEN(INPUT => x)
370                let flatten = Expression::Function(Box::new(Function::new(
371                    "FLATTEN".to_string(),
372                    vec![input_arg],
373                )));
374
375                // Wrap in TABLE(...)
376                let table_func =
377                    Expression::TableFromRows(Box::new(crate::expressions::TableFromRows {
378                        this: Box::new(flatten),
379                        alias: None,
380                        joins: vec![],
381                        pivots: None,
382                        sample: None,
383                    }));
384
385                // Add alias _t0(seq, key, path, index, value, this)
386                Ok(Expression::Alias(Box::new(crate::expressions::Alias {
387                    this: table_func,
388                    alias: crate::expressions::Identifier::new("_t0"),
389                    column_aliases: vec![
390                        crate::expressions::Identifier::new("seq"),
391                        crate::expressions::Identifier::new("key"),
392                        crate::expressions::Identifier::new("path"),
393                        crate::expressions::Identifier::new("index"),
394                        crate::expressions::Identifier::new("value"),
395                        crate::expressions::Identifier::new("this"),
396                    ],
397                    pre_alias_comments: vec![],
398                    trailing_comments: vec![],
399                    inferred_type: None,
400                })))
401            }
402
403            // Array constructor:
404            // - If bracket notation ([1, 2, 3]), preserve it in Snowflake
405            // - If ARRAY[...] syntax, convert to ARRAY_CONSTRUCT
406            Expression::ArrayFunc(arr) => {
407                if arr.bracket_notation {
408                    // Keep bracket notation in Snowflake
409                    Ok(Expression::ArrayFunc(arr))
410                } else {
411                    // Convert ARRAY[...] to ARRAY_CONSTRUCT
412                    Ok(Expression::Function(Box::new(Function::new(
413                        "ARRAY_CONSTRUCT".to_string(),
414                        arr.expressions,
415                    ))))
416                }
417            }
418
419            // ArrayConcat -> ARRAY_CAT
420            Expression::ArrayConcat(f) => Ok(Expression::Function(Box::new(Function::new(
421                "ARRAY_CAT".to_string(),
422                f.expressions,
423            )))),
424
425            // ArrayConcatAgg -> ARRAY_FLATTEN
426            Expression::ArrayConcatAgg(f) => Ok(Expression::Function(Box::new(Function::new(
427                "ARRAY_FLATTEN".to_string(),
428                vec![f.this],
429            )))),
430
431            // ArrayContains -> ARRAY_CONTAINS
432            Expression::ArrayContains(f) => Ok(Expression::Function(Box::new(Function::new(
433                "ARRAY_CONTAINS".to_string(),
434                vec![f.this, f.expression],
435            )))),
436
437            // ArrayIntersect -> ARRAY_INTERSECTION
438            Expression::ArrayIntersect(f) => Ok(Expression::Function(Box::new(Function::new(
439                "ARRAY_INTERSECTION".to_string(),
440                f.expressions,
441            )))),
442
443            // SortArray -> ARRAY_SORT
444            Expression::ArraySort(f) => Ok(Expression::Function(Box::new(Function::new(
445                "ARRAY_SORT".to_string(),
446                vec![f.this],
447            )))),
448
449            // StringToArray -> STRTOK_TO_ARRAY
450            Expression::StringToArray(f) => {
451                let mut args = vec![*f.this];
452                if let Some(expr) = f.expression {
453                    args.push(*expr);
454                }
455                Ok(Expression::Function(Box::new(Function::new(
456                    "STRTOK_TO_ARRAY".to_string(),
457                    args,
458                ))))
459            }
460
461            // ===== Bitwise operations =====
462            // BitwiseOr -> BITOR
463            Expression::BitwiseOr(f) => Ok(Expression::Function(Box::new(Function::new(
464                "BITOR".to_string(),
465                vec![f.left, f.right],
466            )))),
467
468            // BitwiseXor -> BITXOR
469            Expression::BitwiseXor(f) => Ok(Expression::Function(Box::new(Function::new(
470                "BITXOR".to_string(),
471                vec![f.left, f.right],
472            )))),
473
474            // BitwiseAnd -> BITAND
475            Expression::BitwiseAnd(f) => Ok(Expression::Function(Box::new(Function::new(
476                "BITAND".to_string(),
477                vec![f.left, f.right],
478            )))),
479
480            // BitwiseNot -> BITNOT
481            Expression::BitwiseNot(f) => Ok(Expression::Function(Box::new(Function::new(
482                "BITNOT".to_string(),
483                vec![f.this],
484            )))),
485
486            // BitwiseLeftShift -> BITSHIFTLEFT
487            Expression::BitwiseLeftShift(f) => Ok(Expression::Function(Box::new(Function::new(
488                "BITSHIFTLEFT".to_string(),
489                vec![f.left, f.right],
490            )))),
491
492            // BitwiseRightShift -> BITSHIFTRIGHT
493            Expression::BitwiseRightShift(f) => Ok(Expression::Function(Box::new(Function::new(
494                "BITSHIFTRIGHT".to_string(),
495                vec![f.left, f.right],
496            )))),
497
498            // BitwiseAndAgg -> BITAND_AGG
499            Expression::BitwiseAndAgg(f) => Ok(Expression::Function(Box::new(Function::new(
500                "BITAND_AGG".to_string(),
501                vec![f.this],
502            )))),
503
504            // BitwiseOrAgg -> BITOR_AGG
505            Expression::BitwiseOrAgg(f) => Ok(Expression::Function(Box::new(Function::new(
506                "BITOR_AGG".to_string(),
507                vec![f.this],
508            )))),
509
510            // BitwiseXorAgg -> BITXOR_AGG
511            Expression::BitwiseXorAgg(f) => Ok(Expression::Function(Box::new(Function::new(
512                "BITXOR_AGG".to_string(),
513                vec![f.this],
514            )))),
515
516            // ===== Boolean aggregates =====
517            // LogicalAnd -> BOOLAND_AGG
518            Expression::LogicalAnd(f) => Ok(Expression::Function(Box::new(Function::new(
519                "BOOLAND_AGG".to_string(),
520                vec![f.this],
521            )))),
522
523            // LogicalOr -> BOOLOR_AGG
524            Expression::LogicalOr(f) => Ok(Expression::Function(Box::new(Function::new(
525                "BOOLOR_AGG".to_string(),
526                vec![f.this],
527            )))),
528
529            // Booland -> BOOLAND
530            Expression::Booland(f) => Ok(Expression::Function(Box::new(Function::new(
531                "BOOLAND".to_string(),
532                vec![*f.this, *f.expression],
533            )))),
534
535            // Boolor -> BOOLOR
536            Expression::Boolor(f) => Ok(Expression::Function(Box::new(Function::new(
537                "BOOLOR".to_string(),
538                vec![*f.this, *f.expression],
539            )))),
540
541            // Xor -> BOOLXOR
542            Expression::Xor(f) => {
543                let mut args = Vec::new();
544                if let Some(this) = f.this {
545                    args.push(*this);
546                }
547                if let Some(expr) = f.expression {
548                    args.push(*expr);
549                }
550                Ok(Expression::Function(Box::new(Function::new(
551                    "BOOLXOR".to_string(),
552                    args,
553                ))))
554            }
555
556            // ===== Date/time functions =====
557            // DayOfMonth -> DAYOFMONTH
558            Expression::DayOfMonth(f) => Ok(Expression::Function(Box::new(Function::new(
559                "DAYOFMONTH".to_string(),
560                vec![f.this],
561            )))),
562
563            // DayOfWeek -> DAYOFWEEK
564            Expression::DayOfWeek(f) => Ok(Expression::Function(Box::new(Function::new(
565                "DAYOFWEEK".to_string(),
566                vec![f.this],
567            )))),
568
569            // DayOfWeekIso -> DAYOFWEEKISO
570            Expression::DayOfWeekIso(f) => Ok(Expression::Function(Box::new(Function::new(
571                "DAYOFWEEKISO".to_string(),
572                vec![f.this],
573            )))),
574
575            // DayOfYear -> DAYOFYEAR
576            Expression::DayOfYear(f) => Ok(Expression::Function(Box::new(Function::new(
577                "DAYOFYEAR".to_string(),
578                vec![f.this],
579            )))),
580
581            // WeekOfYear -> WEEK (Snowflake native function)
582            Expression::WeekOfYear(f) => Ok(Expression::Function(Box::new(Function::new(
583                "WEEK".to_string(),
584                vec![f.this],
585            )))),
586
587            // YearOfWeek -> YEAROFWEEK
588            Expression::YearOfWeek(f) => Ok(Expression::Function(Box::new(Function::new(
589                "YEAROFWEEK".to_string(),
590                vec![f.this],
591            )))),
592
593            // YearOfWeekIso -> YEAROFWEEKISO
594            Expression::YearOfWeekIso(f) => Ok(Expression::Function(Box::new(Function::new(
595                "YEAROFWEEKISO".to_string(),
596                vec![f.this],
597            )))),
598
599            // ByteLength -> OCTET_LENGTH
600            Expression::ByteLength(f) => Ok(Expression::Function(Box::new(Function::new(
601                "OCTET_LENGTH".to_string(),
602                vec![f.this],
603            )))),
604
605            // TimestampDiff -> TIMESTAMPDIFF
606            Expression::TimestampDiff(f) => {
607                let mut args = vec![];
608                // If unit is set (from cross-dialect normalize), use unit as first arg, this as second, expression as third
609                if let Some(ref unit_str) = f.unit {
610                    args.push(Expression::Identifier(crate::expressions::Identifier::new(
611                        unit_str.clone(),
612                    )));
613                    args.push(*f.this);
614                    args.push(*f.expression);
615                } else {
616                    args.push(*f.this);
617                    args.push(*f.expression);
618                }
619                Ok(Expression::Function(Box::new(Function::new(
620                    "TIMESTAMPDIFF".to_string(),
621                    args,
622                ))))
623            }
624
625            // TimestampAdd -> TIMESTAMPADD
626            Expression::TimestampAdd(f) => {
627                let mut args = vec![];
628                if let Some(ref unit_str) = f.unit {
629                    args.push(Expression::Identifier(crate::expressions::Identifier::new(
630                        unit_str.clone(),
631                    )));
632                    args.push(*f.this);
633                    args.push(*f.expression);
634                } else {
635                    args.push(*f.this);
636                    args.push(*f.expression);
637                }
638                Ok(Expression::Function(Box::new(Function::new(
639                    "TIMESTAMPADD".to_string(),
640                    args,
641                ))))
642            }
643
644            // ToArray -> TO_ARRAY
645            Expression::ToArray(f) => Ok(Expression::Function(Box::new(Function::new(
646                "TO_ARRAY".to_string(),
647                vec![f.this],
648            )))),
649
650            // DateAdd -> DATEADD (with unit, amount, date order)
651            Expression::DateAdd(f) => {
652                let unit_str = interval_unit_to_str(&f.unit);
653                let unit = Expression::Identifier(crate::expressions::Identifier {
654                    name: unit_str,
655                    quoted: false,
656                    trailing_comments: Vec::new(),
657                    span: None,
658                });
659                Ok(Expression::Function(Box::new(Function::new(
660                    "DATEADD".to_string(),
661                    vec![unit, f.interval, f.this],
662                ))))
663            }
664
665            // DateSub -> DATEADD with negated amount: val * -1
666            Expression::DateSub(f) => {
667                let unit_str = interval_unit_to_str(&f.unit);
668                let unit = Expression::Identifier(crate::expressions::Identifier {
669                    name: unit_str,
670                    quoted: false,
671                    trailing_comments: Vec::new(),
672                    span: None,
673                });
674                // Negate using val * -1 format (matching Python sqlglot output)
675                let neg_expr = Expression::Mul(Box::new(crate::expressions::BinaryOp::new(
676                    f.interval,
677                    Expression::Neg(Box::new(crate::expressions::UnaryOp {
678                        this: Expression::number(1),
679                        inferred_type: None,
680                    })),
681                )));
682                Ok(Expression::Function(Box::new(Function::new(
683                    "DATEADD".to_string(),
684                    vec![unit, neg_expr, f.this],
685                ))))
686            }
687
688            // DateDiff -> DATEDIFF
689            Expression::DateDiff(f) => {
690                let unit_str =
691                    interval_unit_to_str(&f.unit.unwrap_or(crate::expressions::IntervalUnit::Day));
692                let unit = Expression::Identifier(crate::expressions::Identifier {
693                    name: unit_str,
694                    quoted: false,
695                    trailing_comments: Vec::new(),
696                    span: None,
697                });
698                Ok(Expression::Function(Box::new(Function::new(
699                    "DATEDIFF".to_string(),
700                    vec![unit, f.expression, f.this],
701                ))))
702            }
703
704            // ===== String functions =====
705            // StringAgg -> LISTAGG in Snowflake
706            Expression::StringAgg(f) => {
707                let mut args = vec![f.this.clone()];
708                if let Some(separator) = &f.separator {
709                    args.push(separator.clone());
710                }
711                Ok(Expression::Function(Box::new(Function::new(
712                    "LISTAGG".to_string(),
713                    args,
714                ))))
715            }
716
717            // StartsWith -> STARTSWITH
718            Expression::StartsWith(f) => Ok(Expression::Function(Box::new(Function::new(
719                "STARTSWITH".to_string(),
720                vec![f.this, f.expression],
721            )))),
722
723            // EndsWith -> keep as EndsWith AST node; generator outputs per-dialect
724            Expression::EndsWith(f) => Ok(Expression::EndsWith(f)),
725
726            // Stuff -> INSERT
727            Expression::Stuff(f) => {
728                let mut args = vec![*f.this];
729                if let Some(start) = f.start {
730                    args.push(*start);
731                }
732                if let Some(length) = f.length {
733                    args.push(Expression::number(length));
734                }
735                args.push(*f.expression);
736                Ok(Expression::Function(Box::new(Function::new(
737                    "INSERT".to_string(),
738                    args,
739                ))))
740            }
741
742            // ===== Hash functions =====
743            // SHA -> SHA1
744            Expression::SHA(f) => Ok(Expression::Function(Box::new(Function::new(
745                "SHA1".to_string(),
746                vec![f.this],
747            )))),
748
749            // SHA1Digest -> SHA1_BINARY
750            Expression::SHA1Digest(f) => Ok(Expression::Function(Box::new(Function::new(
751                "SHA1_BINARY".to_string(),
752                vec![f.this],
753            )))),
754
755            // SHA2Digest -> SHA2_BINARY
756            Expression::SHA2Digest(f) => Ok(Expression::Function(Box::new(Function::new(
757                "SHA2_BINARY".to_string(),
758                vec![*f.this],
759            )))),
760
761            // MD5Digest -> MD5_BINARY
762            Expression::MD5Digest(f) => Ok(Expression::Function(Box::new(Function::new(
763                "MD5_BINARY".to_string(),
764                vec![*f.this],
765            )))),
766
767            // MD5NumberLower64 -> MD5_NUMBER_LOWER64
768            Expression::MD5NumberLower64(f) => Ok(Expression::Function(Box::new(Function::new(
769                "MD5_NUMBER_LOWER64".to_string(),
770                vec![f.this],
771            )))),
772
773            // MD5NumberUpper64 -> MD5_NUMBER_UPPER64
774            Expression::MD5NumberUpper64(f) => Ok(Expression::Function(Box::new(Function::new(
775                "MD5_NUMBER_UPPER64".to_string(),
776                vec![f.this],
777            )))),
778
779            // ===== Vector functions =====
780            // CosineDistance -> VECTOR_COSINE_SIMILARITY
781            Expression::CosineDistance(f) => Ok(Expression::Function(Box::new(Function::new(
782                "VECTOR_COSINE_SIMILARITY".to_string(),
783                vec![*f.this, *f.expression],
784            )))),
785
786            // DotProduct -> VECTOR_INNER_PRODUCT
787            Expression::DotProduct(f) => Ok(Expression::Function(Box::new(Function::new(
788                "VECTOR_INNER_PRODUCT".to_string(),
789                vec![*f.this, *f.expression],
790            )))),
791
792            // EuclideanDistance -> VECTOR_L2_DISTANCE
793            Expression::EuclideanDistance(f) => Ok(Expression::Function(Box::new(Function::new(
794                "VECTOR_L2_DISTANCE".to_string(),
795                vec![*f.this, *f.expression],
796            )))),
797
798            // ManhattanDistance -> VECTOR_L1_DISTANCE
799            Expression::ManhattanDistance(f) => Ok(Expression::Function(Box::new(Function::new(
800                "VECTOR_L1_DISTANCE".to_string(),
801                vec![*f.this, *f.expression],
802            )))),
803
804            // ===== JSON/Struct functions =====
805            // JSONFormat -> TO_JSON
806            Expression::JSONFormat(f) => {
807                let mut args = Vec::new();
808                if let Some(this) = f.this {
809                    args.push(*this);
810                }
811                Ok(Expression::Function(Box::new(Function::new(
812                    "TO_JSON".to_string(),
813                    args,
814                ))))
815            }
816
817            // JSONKeys -> OBJECT_KEYS
818            Expression::JSONKeys(f) => Ok(Expression::Function(Box::new(Function::new(
819                "OBJECT_KEYS".to_string(),
820                vec![*f.this],
821            )))),
822
823            // GetExtract -> GET
824            Expression::GetExtract(f) => Ok(Expression::Function(Box::new(Function::new(
825                "GET".to_string(),
826                vec![*f.this, *f.expression],
827            )))),
828
829            // StarMap -> OBJECT_CONSTRUCT
830            Expression::StarMap(f) => Ok(Expression::Function(Box::new(Function::new(
831                "OBJECT_CONSTRUCT".to_string(),
832                vec![f.this, f.expression],
833            )))),
834
835            // LowerHex -> TO_CHAR
836            Expression::LowerHex(f) => Ok(Expression::Function(Box::new(Function::new(
837                "TO_CHAR".to_string(),
838                vec![f.this],
839            )))),
840
841            // Skewness -> SKEW
842            Expression::Skewness(f) => Ok(Expression::Function(Box::new(Function::new(
843                "SKEW".to_string(),
844                vec![f.this],
845            )))),
846
847            // StPoint -> ST_MAKEPOINT
848            Expression::StPoint(f) => Ok(Expression::Function(Box::new(Function::new(
849                "ST_MAKEPOINT".to_string(),
850                vec![*f.this, *f.expression],
851            )))),
852
853            // FromTimeZone -> CONVERT_TIMEZONE
854            Expression::FromTimeZone(f) => Ok(Expression::Function(Box::new(Function::new(
855                "CONVERT_TIMEZONE".to_string(),
856                vec![*f.this],
857            )))),
858
859            // ===== Conversion functions =====
860            // Unhex -> HEX_DECODE_BINARY
861            Expression::Unhex(f) => Ok(Expression::Function(Box::new(Function::new(
862                "HEX_DECODE_BINARY".to_string(),
863                vec![*f.this],
864            )))),
865
866            // UnixToTime -> TO_TIMESTAMP
867            Expression::UnixToTime(f) => {
868                let mut args = vec![*f.this];
869                if let Some(scale) = f.scale {
870                    args.push(Expression::number(scale));
871                }
872                Ok(Expression::Function(Box::new(Function::new(
873                    "TO_TIMESTAMP".to_string(),
874                    args,
875                ))))
876            }
877
878            // ===== Conditional =====
879            // IfFunc -> keep as IfFunc with IFF name for Snowflake
880            Expression::IfFunc(f) => Ok(Expression::IfFunc(Box::new(crate::expressions::IfFunc {
881                condition: f.condition,
882                true_value: f.true_value,
883                false_value: Some(
884                    f.false_value
885                        .unwrap_or(Expression::Null(crate::expressions::Null)),
886                ),
887                original_name: Some("IFF".to_string()),
888                inferred_type: None,
889            }))),
890
891            // ===== Aggregate functions =====
892            // ApproxDistinct -> APPROX_COUNT_DISTINCT
893            Expression::ApproxDistinct(f) => Ok(Expression::Function(Box::new(Function::new(
894                "APPROX_COUNT_DISTINCT".to_string(),
895                vec![f.this],
896            )))),
897
898            // ArgMax -> MAX_BY
899            Expression::ArgMax(f) => Ok(Expression::Function(Box::new(Function::new(
900                "MAX_BY".to_string(),
901                vec![*f.this, *f.expression],
902            )))),
903
904            // ArgMin -> MIN_BY
905            Expression::ArgMin(f) => Ok(Expression::Function(Box::new(Function::new(
906                "MIN_BY".to_string(),
907                vec![*f.this, *f.expression],
908            )))),
909
910            // ===== Random =====
911            // RANDOM is native to Snowflake - keep as-is
912            Expression::Random(_) => Ok(Expression::Random(crate::expressions::Random)),
913
914            // Rand - keep as-is (generator outputs RANDOM for Snowflake)
915            Expression::Rand(r) => Ok(Expression::Rand(r)),
916
917            // ===== UUID =====
918            // Uuid -> keep as Uuid node; generator will output UUID_STRING for Snowflake
919            Expression::Uuid(u) => Ok(Expression::Uuid(u)),
920
921            // ===== Map/Object =====
922            // Map -> OBJECT_CONSTRUCT
923            Expression::Map(f) => Ok(Expression::Function(Box::new(Function::new(
924                "OBJECT_CONSTRUCT".to_string(),
925                f.keys
926                    .into_iter()
927                    .zip(f.values.into_iter())
928                    .flat_map(|(k, v)| vec![k, v])
929                    .collect(),
930            )))),
931
932            // MapFunc (curly brace syntax) -> OBJECT_CONSTRUCT
933            Expression::MapFunc(f) => Ok(Expression::Function(Box::new(Function::new(
934                "OBJECT_CONSTRUCT".to_string(),
935                f.keys
936                    .into_iter()
937                    .zip(f.values.into_iter())
938                    .flat_map(|(k, v)| vec![k, v])
939                    .collect(),
940            )))),
941
942            // VarMap -> OBJECT_CONSTRUCT
943            Expression::VarMap(f) => Ok(Expression::Function(Box::new(Function::new(
944                "OBJECT_CONSTRUCT".to_string(),
945                f.keys
946                    .into_iter()
947                    .zip(f.values.into_iter())
948                    .flat_map(|(k, v)| vec![k, v])
949                    .collect(),
950            )))),
951
952            // ===== JSON =====
953            // JSONObject -> OBJECT_CONSTRUCT_KEEP_NULL
954            Expression::JsonObject(f) => Ok(Expression::Function(Box::new(Function::new(
955                "OBJECT_CONSTRUCT_KEEP_NULL".to_string(),
956                f.pairs.into_iter().flat_map(|(k, v)| vec![k, v]).collect(),
957            )))),
958
959            // JSONExtractScalar -> JSON_EXTRACT_PATH_TEXT
960            Expression::JsonExtractScalar(f) => Ok(Expression::Function(Box::new(Function::new(
961                "JSON_EXTRACT_PATH_TEXT".to_string(),
962                vec![f.this, f.path],
963            )))),
964
965            // ===== Struct =====
966            // Struct -> OBJECT_CONSTRUCT
967            Expression::Struct(f) => Ok(Expression::Function(Box::new(Function::new(
968                "OBJECT_CONSTRUCT".to_string(),
969                f.fields
970                    .into_iter()
971                    .flat_map(|(name, expr)| {
972                        let key = match name {
973                            Some(n) => Expression::string(n),
974                            None => Expression::Null(crate::expressions::Null),
975                        };
976                        vec![key, expr]
977                    })
978                    .collect(),
979            )))),
980
981            // ===== JSON Path =====
982            // JSONPathRoot -> empty string ($ is implicit in Snowflake)
983            Expression::JSONPathRoot(_) => Ok(Expression::Literal(Box::new(
984                crate::expressions::Literal::String(String::new()),
985            ))),
986
987            // ===== VarSamp -> VARIANCE (Snowflake) =====
988            // Snowflake uses VARIANCE instead of VAR_SAMP
989            Expression::VarSamp(agg) => Ok(Expression::Variance(agg)),
990
991            // ===== VarPop -> keep as VarPop =====
992            // The generator handles dialect-specific naming (VARIANCE_POP for Snowflake)
993            Expression::VarPop(agg) => Ok(Expression::VarPop(agg)),
994
995            // ===== EXTRACT -> DATE_PART =====
996            // Snowflake uses DATE_PART instead of EXTRACT
997            Expression::Extract(f) => {
998                use crate::expressions::DateTimeField;
999                // Recursively transform the inner expression (e.g., CAST(... AS TIMESTAMP_NTZ) -> CAST(... AS TIMESTAMPNTZ))
1000                let transformed_this = self.transform_expr(f.this)?;
1001                let field_name = match &f.field {
1002                    DateTimeField::Year => "YEAR",
1003                    DateTimeField::Month => "MONTH",
1004                    DateTimeField::Day => "DAY",
1005                    DateTimeField::Hour => "HOUR",
1006                    DateTimeField::Minute => "MINUTE",
1007                    DateTimeField::Second => "SECOND",
1008                    DateTimeField::Millisecond => "MILLISECOND",
1009                    DateTimeField::Microsecond => "MICROSECOND",
1010                    DateTimeField::Week => "WEEK",
1011                    DateTimeField::WeekWithModifier(m) => {
1012                        return Ok(Expression::Function(Box::new(Function::new(
1013                            "DATE_PART".to_string(),
1014                            vec![
1015                                Expression::Identifier(crate::expressions::Identifier {
1016                                    name: format!("WEEK({})", m),
1017                                    quoted: false,
1018                                    trailing_comments: Vec::new(),
1019                                    span: None,
1020                                }),
1021                                transformed_this,
1022                            ],
1023                        ))))
1024                    }
1025                    DateTimeField::DayOfWeek => "DAYOFWEEK",
1026                    DateTimeField::DayOfYear => "DAYOFYEAR",
1027                    DateTimeField::Quarter => "QUARTER",
1028                    DateTimeField::Epoch => "EPOCH",
1029                    DateTimeField::Timezone => "TIMEZONE",
1030                    DateTimeField::TimezoneHour => "TIMEZONE_HOUR",
1031                    DateTimeField::TimezoneMinute => "TIMEZONE_MINUTE",
1032                    DateTimeField::Date => "DATE",
1033                    DateTimeField::Time => "TIME",
1034                    DateTimeField::Custom(s) => {
1035                        // Map common EXTRACT field names to Snowflake DATE_PART names
1036                        match s.to_uppercase().as_str() {
1037                            "DAYOFMONTH" => "DAY",
1038                            "DOW" => "DAYOFWEEK",
1039                            "DOY" => "DAYOFYEAR",
1040                            "ISODOW" => "DAYOFWEEKISO",
1041                            "EPOCH_SECOND" | "EPOCH_SECONDS" => "EPOCH_SECOND",
1042                            "EPOCH_MILLISECOND" | "EPOCH_MILLISECONDS" => "EPOCH_MILLISECOND",
1043                            "EPOCH_MICROSECOND" | "EPOCH_MICROSECONDS" => "EPOCH_MICROSECOND",
1044                            "EPOCH_NANOSECOND" | "EPOCH_NANOSECONDS" => "EPOCH_NANOSECOND",
1045                            _ => {
1046                                return {
1047                                    let field_ident =
1048                                        Expression::Identifier(crate::expressions::Identifier {
1049                                            name: s.to_string(),
1050                                            quoted: false,
1051                                            trailing_comments: Vec::new(),
1052                                            span: None,
1053                                        });
1054                                    Ok(Expression::Function(Box::new(Function::new(
1055                                        "DATE_PART".to_string(),
1056                                        vec![field_ident, transformed_this],
1057                                    ))))
1058                                }
1059                            }
1060                        }
1061                    }
1062                };
1063                let field_ident = Expression::Identifier(crate::expressions::Identifier {
1064                    name: field_name.to_string(),
1065                    quoted: false,
1066                    trailing_comments: Vec::new(),
1067                    span: None,
1068                });
1069                Ok(Expression::Function(Box::new(Function::new(
1070                    "DATE_PART".to_string(),
1071                    vec![field_ident, transformed_this],
1072                ))))
1073            }
1074
1075            // Generic function transformations
1076            Expression::Function(f) => self.transform_function(*f),
1077
1078            // SUM - recursively transform inner expression
1079            Expression::Sum(mut agg) => {
1080                agg.this = self.transform_expr(agg.this)?;
1081                Ok(Expression::Sum(agg))
1082            }
1083
1084            // Generic aggregate function transformations
1085            Expression::AggregateFunction(f) => self.transform_aggregate_function(f),
1086
1087            // Handle NamedArgument - recursively transform the value
1088            Expression::NamedArgument(na) => {
1089                let transformed_value = self.transform_expr(na.value)?;
1090                Ok(Expression::NamedArgument(Box::new(
1091                    crate::expressions::NamedArgument {
1092                        name: na.name,
1093                        value: transformed_value,
1094                        separator: na.separator,
1095                    },
1096                )))
1097            }
1098
1099            // Handle CreateTable - transform column data types and default/computed expressions
1100            Expression::CreateTable(mut ct) => {
1101                for col in &mut ct.columns {
1102                    if let Expression::DataType(new_dt) =
1103                        self.transform_data_type(col.data_type.clone())?
1104                    {
1105                        col.data_type = new_dt;
1106                    }
1107                    // Also transform computed/default expressions (e.g., AS (parse_json(x):COL3::number))
1108                    if let Some(default_expr) = col.default.take() {
1109                        col.default = Some(self.transform_expr(default_expr)?);
1110                    }
1111                    // Transform expressions in column constraints (computed columns)
1112                    for constraint in &mut col.constraints {
1113                        if let crate::expressions::ColumnConstraint::ComputedColumn(cc) = constraint
1114                        {
1115                            let transformed = self.transform_expr(*cc.expression.clone())?;
1116                            cc.expression = Box::new(transformed);
1117                        }
1118                    }
1119                }
1120
1121                // For EXTERNAL tables, convert with_properties to Raw properties
1122                // with proper Snowflake formatting (no WITH wrapper, specific key casing)
1123                if ct.table_modifier.as_deref() == Some("EXTERNAL")
1124                    && !ct.with_properties.is_empty()
1125                {
1126                    for (key, value) in ct.with_properties.drain(..) {
1127                        let formatted = Self::format_external_table_property(&key, &value);
1128                        ct.properties
1129                            .push(Expression::Raw(crate::expressions::Raw { sql: formatted }));
1130                    }
1131                }
1132
1133                Ok(Expression::CreateTable(ct))
1134            }
1135
1136            // Handle AlterTable - transform column data types in ADD operations
1137            Expression::AlterTable(mut at) => {
1138                for action in &mut at.actions {
1139                    if let crate::expressions::AlterTableAction::AddColumn { column, .. } = action {
1140                        if let Expression::DataType(new_dt) =
1141                            self.transform_data_type(column.data_type.clone())?
1142                        {
1143                            column.data_type = new_dt;
1144                        }
1145                    }
1146                }
1147                Ok(Expression::AlterTable(at))
1148            }
1149
1150            // Handle Table reference - transform HistoricalData (AT/BEFORE time travel clauses)
1151            Expression::Table(mut t) => {
1152                if let Some(when) = t.when.take() {
1153                    // Recursively transform the expression inside HistoricalData
1154                    let transformed_expr = self.transform_expr(*when.expression)?;
1155                    t.when = Some(Box::new(crate::expressions::HistoricalData {
1156                        this: when.this,
1157                        kind: when.kind,
1158                        expression: Box::new(transformed_expr),
1159                    }));
1160                }
1161                Ok(Expression::Table(t))
1162            }
1163
1164            // Handle Subscript - recursively transform inner expression
1165            Expression::Subscript(s) => {
1166                let transformed_this = self.transform_expr(s.this)?;
1167                let transformed_index = self.transform_expr(s.index)?;
1168                Ok(Expression::Subscript(Box::new(
1169                    crate::expressions::Subscript {
1170                        this: transformed_this,
1171                        index: transformed_index,
1172                    },
1173                )))
1174            }
1175
1176            // Recursively transform parenthesized expressions
1177            Expression::Paren(p) => {
1178                let transformed = self.transform_expr(p.this)?;
1179                Ok(Expression::Paren(Box::new(crate::expressions::Paren {
1180                    this: transformed,
1181                    trailing_comments: p.trailing_comments,
1182                })))
1183            }
1184
1185            // ===== ORDER BY null ordering normalization =====
1186            // Snowflake is nulls_are_large: ASC defaults to NULLS LAST, DESC defaults to NULLS FIRST
1187            // Fill in implicit nulls_first so target dialects can properly strip/add as needed
1188            Expression::Select(mut select) => {
1189                if let Some(ref mut order) = select.order_by {
1190                    for ord in &mut order.expressions {
1191                        if ord.nulls_first.is_none() {
1192                            ord.nulls_first = Some(ord.desc);
1193                        }
1194                    }
1195                }
1196                Ok(Expression::Select(select))
1197            }
1198
1199            // Fill in NULLS ordering for window function ORDER BY clauses
1200            Expression::WindowFunction(mut wf) => {
1201                for ord in &mut wf.over.order_by {
1202                    if ord.nulls_first.is_none() {
1203                        ord.nulls_first = Some(ord.desc);
1204                    }
1205                }
1206                Ok(Expression::WindowFunction(wf))
1207            }
1208
1209            // Also handle Expression::Window (WindowSpec)
1210            Expression::Window(mut w) => {
1211                for ord in &mut w.order_by {
1212                    if ord.nulls_first.is_none() {
1213                        ord.nulls_first = Some(ord.desc);
1214                    }
1215                }
1216                Ok(Expression::Window(w))
1217            }
1218
1219            // LATERAL FLATTEN: add default column aliases (SEQ, KEY, PATH, INDEX, VALUE, THIS)
1220            Expression::Lateral(mut lat) => {
1221                // Check if the inner expression is a FLATTEN function
1222                let is_flatten = match lat.this.as_ref() {
1223                    Expression::Function(f) => f.name.to_uppercase() == "FLATTEN",
1224                    _ => false,
1225                };
1226                if is_flatten && lat.column_aliases.is_empty() {
1227                    // Add default column aliases
1228                    lat.column_aliases = vec![
1229                        "SEQ".to_string(),
1230                        "KEY".to_string(),
1231                        "PATH".to_string(),
1232                        "INDEX".to_string(),
1233                        "VALUE".to_string(),
1234                        "THIS".to_string(),
1235                    ];
1236                    // If no alias, add _flattened
1237                    if lat.alias.is_none() {
1238                        lat.alias = Some("_flattened".to_string());
1239                    }
1240                }
1241                Ok(Expression::Lateral(lat))
1242            }
1243
1244            // Pass through everything else
1245            _ => Ok(expr),
1246        }
1247    }
1248}
1249
1250impl SnowflakeDialect {
1251    /// Format a Snowflake external table property for output.
1252    /// Some properties like LOCATION and FILE_FORMAT are uppercased keywords.
1253    fn format_external_table_property(key: &str, value: &str) -> String {
1254        let lower_key = key.to_lowercase();
1255        match lower_key.as_str() {
1256            "location" => format!("LOCATION={}", value),
1257            "file_format" => {
1258                // Format file_format value: remove spaces around =, uppercase booleans
1259                let formatted_value = Self::format_file_format_value(value);
1260                format!("FILE_FORMAT={}", formatted_value)
1261            }
1262            _ => format!("{}={}", key, value),
1263        }
1264    }
1265
1266    /// Format file_format property value:
1267    /// - Remove spaces around = signs
1268    /// - Uppercase boolean values (false -> FALSE, true -> TRUE)
1269    fn format_file_format_value(value: &str) -> String {
1270        if !value.starts_with('(') {
1271            return value.to_string();
1272        }
1273        // Strip outer parens, process inner key=value pairs
1274        let inner = value[1..value.len() - 1].trim();
1275        // Parse space-separated key=value pairs (may have spaces around =)
1276        let mut result = String::from("(");
1277        let mut parts: Vec<String> = Vec::new();
1278        // Split by whitespace and reconstruct key=value pairs
1279        let tokens: Vec<&str> = inner.split_whitespace().collect();
1280        let mut i = 0;
1281        while i < tokens.len() {
1282            let token = tokens[i];
1283            if i + 2 < tokens.len() && tokens[i + 1] == "=" {
1284                // key = value pattern
1285                let val = Self::format_property_value(tokens[i + 2]);
1286                parts.push(format!("{}={}", token, val));
1287                i += 3;
1288            } else if token.contains('=') {
1289                // key=value already joined
1290                let eq_pos = token.find('=').unwrap();
1291                let k = &token[..eq_pos];
1292                let v = Self::format_property_value(&token[eq_pos + 1..]);
1293                parts.push(format!("{}={}", k, v));
1294                i += 1;
1295            } else {
1296                parts.push(token.to_string());
1297                i += 1;
1298            }
1299        }
1300        result.push_str(&parts.join(" "));
1301        result.push(')');
1302        result
1303    }
1304
1305    /// Format a property value - uppercase boolean literals
1306    fn format_property_value(value: &str) -> String {
1307        match value.to_lowercase().as_str() {
1308            "true" => "TRUE".to_string(),
1309            "false" => "FALSE".to_string(),
1310            _ => value.to_string(),
1311        }
1312    }
1313
1314    /// Transform data types according to Snowflake TYPE_MAPPING
1315    fn transform_data_type(&self, dt: crate::expressions::DataType) -> Result<Expression> {
1316        use crate::expressions::DataType;
1317        let transformed = match dt {
1318            // TEXT -> VARCHAR
1319            DataType::Text => DataType::VarChar {
1320                length: None,
1321                parenthesized_length: false,
1322            },
1323            // STRUCT -> OBJECT
1324            DataType::Struct { fields, .. } => {
1325                // Snowflake uses OBJECT for struct types
1326                let _ = fields; // Snowflake OBJECT doesn't preserve field names in the same way
1327                DataType::Custom {
1328                    name: "OBJECT".to_string(),
1329                }
1330            }
1331            // Custom type transformations
1332            DataType::Custom { name } => {
1333                let upper_name = name.to_uppercase();
1334                match upper_name.as_str() {
1335                    // NVARCHAR -> VARCHAR (SQL Server type)
1336                    "NVARCHAR" | "NCHAR" | "NATIONAL CHARACTER VARYING" | "NATIONAL CHAR" => {
1337                        DataType::VarChar {
1338                            length: None,
1339                            parenthesized_length: false,
1340                        }
1341                    }
1342                    // STRING -> VARCHAR (Snowflake accepts both, but normalizes to VARCHAR)
1343                    "STRING" => DataType::VarChar {
1344                        length: None,
1345                        parenthesized_length: false,
1346                    },
1347                    // BIGDECIMAL -> DOUBLE
1348                    "BIGDECIMAL" => DataType::Double {
1349                        precision: None,
1350                        scale: None,
1351                    },
1352                    // NESTED -> OBJECT
1353                    "NESTED" => DataType::Custom {
1354                        name: "OBJECT".to_string(),
1355                    },
1356                    // BYTEINT -> INT
1357                    "BYTEINT" => DataType::Int {
1358                        length: None,
1359                        integer_spelling: false,
1360                    },
1361                    // CHAR VARYING -> VARCHAR
1362                    "CHAR VARYING" | "CHARACTER VARYING" => DataType::VarChar {
1363                        length: None,
1364                        parenthesized_length: false,
1365                    },
1366                    // SQL_DOUBLE -> DOUBLE
1367                    "SQL_DOUBLE" => DataType::Double {
1368                        precision: None,
1369                        scale: None,
1370                    },
1371                    // SQL_VARCHAR -> VARCHAR
1372                    "SQL_VARCHAR" => DataType::VarChar {
1373                        length: None,
1374                        parenthesized_length: false,
1375                    },
1376                    // TIMESTAMP_NTZ -> TIMESTAMPNTZ (normalize underscore form)
1377                    "TIMESTAMP_NTZ" => DataType::Custom {
1378                        name: "TIMESTAMPNTZ".to_string(),
1379                    },
1380                    // TIMESTAMP_LTZ -> TIMESTAMPLTZ (normalize underscore form)
1381                    "TIMESTAMP_LTZ" => DataType::Custom {
1382                        name: "TIMESTAMPLTZ".to_string(),
1383                    },
1384                    // TIMESTAMP_TZ -> TIMESTAMPTZ (normalize underscore form)
1385                    "TIMESTAMP_TZ" => DataType::Custom {
1386                        name: "TIMESTAMPTZ".to_string(),
1387                    },
1388                    // NCHAR VARYING -> VARCHAR
1389                    "NCHAR VARYING" => DataType::VarChar {
1390                        length: None,
1391                        parenthesized_length: false,
1392                    },
1393                    // NUMBER -> DECIMAL(38, 0) (Snowflake's default NUMBER is DECIMAL(38, 0))
1394                    "NUMBER" => DataType::Decimal {
1395                        precision: Some(38),
1396                        scale: Some(0),
1397                    },
1398                    _ if name.starts_with("NUMBER(") => {
1399                        // NUMBER(precision, scale) -> DECIMAL(precision, scale)
1400                        // Parse: "NUMBER(38, 0)" -> precision=38, scale=0
1401                        let inner = &name[7..name.len() - 1]; // strip "NUMBER(" and ")"
1402                        let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
1403                        let precision = parts.first().and_then(|p| p.parse::<u32>().ok());
1404                        let scale = parts.get(1).and_then(|s| s.parse::<u32>().ok());
1405                        DataType::Decimal { precision, scale }
1406                    }
1407                    _ => DataType::Custom { name },
1408                }
1409            }
1410            // DECIMAL without precision -> DECIMAL(38, 0) (Snowflake default)
1411            DataType::Decimal {
1412                precision: None,
1413                scale: None,
1414            } => DataType::Decimal {
1415                precision: Some(38),
1416                scale: Some(0),
1417            },
1418            // FLOAT -> DOUBLE (Snowflake FLOAT is actually 64-bit DOUBLE)
1419            DataType::Float { .. } => DataType::Double {
1420                precision: None,
1421                scale: None,
1422            },
1423            // Keep all other types as-is (Snowflake is quite flexible)
1424            other => other,
1425        };
1426        Ok(Expression::DataType(transformed))
1427    }
1428
1429    /// Map date part abbreviation to canonical form (from Python SQLGlot DATE_PART_MAPPING)
1430    fn map_date_part(abbr: &str) -> Option<&'static str> {
1431        match abbr.to_uppercase().as_str() {
1432            // Year
1433            "Y" | "YY" | "YYY" | "YYYY" | "YR" | "YEARS" | "YRS" => Some("YEAR"),
1434            // Month
1435            "MM" | "MON" | "MONS" | "MONTHS" => Some("MONTH"),
1436            // Day
1437            "D" | "DD" | "DAYS" | "DAYOFMONTH" => Some("DAY"),
1438            // Day of week
1439            "DAY OF WEEK" | "WEEKDAY" | "DOW" | "DW" => Some("DAYOFWEEK"),
1440            "WEEKDAY_ISO" | "DOW_ISO" | "DW_ISO" | "DAYOFWEEK_ISO" => Some("DAYOFWEEKISO"),
1441            // Day of year
1442            "DAY OF YEAR" | "DOY" | "DY" => Some("DAYOFYEAR"),
1443            // Week
1444            "W" | "WK" | "WEEKOFYEAR" | "WOY" | "WY" => Some("WEEK"),
1445            "WEEK_ISO" | "WEEKOFYEARISO" | "WEEKOFYEAR_ISO" => Some("WEEKISO"),
1446            // Quarter
1447            "Q" | "QTR" | "QTRS" | "QUARTERS" => Some("QUARTER"),
1448            // Hour
1449            "H" | "HH" | "HR" | "HOURS" | "HRS" => Some("HOUR"),
1450            // Minute (note: 'M' could be minute in some contexts, but we keep it simple)
1451            "MI" | "MIN" | "MINUTES" | "MINS" => Some("MINUTE"),
1452            // Second
1453            "S" | "SEC" | "SECONDS" | "SECS" => Some("SECOND"),
1454            // Millisecond
1455            "MS" | "MSEC" | "MSECS" | "MSECOND" | "MSECONDS" | "MILLISEC" | "MILLISECS"
1456            | "MILLISECON" | "MILLISECONDS" => Some("MILLISECOND"),
1457            // Microsecond
1458            "US" | "USEC" | "USECS" | "MICROSEC" | "MICROSECS" | "USECOND" | "USECONDS"
1459            | "MICROSECONDS" => Some("MICROSECOND"),
1460            // Nanosecond
1461            "NS" | "NSEC" | "NANOSEC" | "NSECOND" | "NSECONDS" | "NANOSECS" => Some("NANOSECOND"),
1462            // Epoch variants
1463            "EPOCH_SECOND" | "EPOCH_SECONDS" => Some("EPOCH_SECOND"),
1464            "EPOCH_MILLISECOND" | "EPOCH_MILLISECONDS" => Some("EPOCH_MILLISECOND"),
1465            "EPOCH_MICROSECOND" | "EPOCH_MICROSECONDS" => Some("EPOCH_MICROSECOND"),
1466            "EPOCH_NANOSECOND" | "EPOCH_NANOSECONDS" => Some("EPOCH_NANOSECOND"),
1467            // Timezone
1468            "TZH" => Some("TIMEZONE_HOUR"),
1469            "TZM" => Some("TIMEZONE_MINUTE"),
1470            // Decade
1471            "DEC" | "DECS" | "DECADES" => Some("DECADE"),
1472            // Millennium
1473            "MIL" | "MILS" | "MILLENIA" => Some("MILLENNIUM"),
1474            // Century
1475            "C" | "CENT" | "CENTS" | "CENTURIES" => Some("CENTURY"),
1476            // No mapping needed (already canonical or unknown)
1477            _ => None,
1478        }
1479    }
1480
1481    /// Transform a date part identifier/expression using the mapping
1482    fn transform_date_part_arg(&self, expr: Expression) -> Expression {
1483        match &expr {
1484            // Handle string literal: 'minute' -> minute (unquoted identifier, preserving case)
1485            Expression::Literal(lit)
1486                if matches!(lit.as_ref(), crate::expressions::Literal::String(_)) =>
1487            {
1488                let crate::expressions::Literal::String(s) = lit.as_ref() else {
1489                    unreachable!()
1490                };
1491                Expression::Identifier(crate::expressions::Identifier {
1492                    name: s.clone(),
1493                    quoted: false,
1494                    trailing_comments: Vec::new(),
1495                    span: None,
1496                })
1497            }
1498            // Handle Identifier (rare case)
1499            Expression::Identifier(id) => {
1500                if let Some(canonical) = Self::map_date_part(&id.name) {
1501                    Expression::Identifier(crate::expressions::Identifier {
1502                        name: canonical.to_string(),
1503                        quoted: false,
1504                        trailing_comments: Vec::new(),
1505                        span: None,
1506                    })
1507                } else {
1508                    // No mapping needed, keep original (Python sqlglot preserves case)
1509                    expr
1510                }
1511            }
1512            Expression::Var(v) => {
1513                if let Some(canonical) = Self::map_date_part(&v.this) {
1514                    Expression::Identifier(crate::expressions::Identifier {
1515                        name: canonical.to_string(),
1516                        quoted: false,
1517                        trailing_comments: Vec::new(),
1518                        span: None,
1519                    })
1520                } else {
1521                    expr
1522                }
1523            }
1524            // Handle Column (more common - parser treats unqualified names as columns)
1525            Expression::Column(col) if col.table.is_none() => {
1526                if let Some(canonical) = Self::map_date_part(&col.name.name) {
1527                    Expression::Identifier(crate::expressions::Identifier {
1528                        name: canonical.to_string(),
1529                        quoted: false,
1530                        trailing_comments: Vec::new(),
1531                        span: None,
1532                    })
1533                } else {
1534                    // No mapping needed, keep original (Python sqlglot preserves case)
1535                    expr
1536                }
1537            }
1538            _ => expr,
1539        }
1540    }
1541
1542    /// Like transform_date_part_arg but only handles Identifier/Column, never String literals.
1543    /// Used for native Snowflake DATE_PART where string args should stay as strings.
1544    fn transform_date_part_arg_identifiers_only(&self, expr: Expression) -> Expression {
1545        match &expr {
1546            Expression::Identifier(id) => {
1547                if let Some(canonical) = Self::map_date_part(&id.name) {
1548                    Expression::Identifier(crate::expressions::Identifier {
1549                        name: canonical.to_string(),
1550                        quoted: false,
1551                        trailing_comments: Vec::new(),
1552                        span: None,
1553                    })
1554                } else {
1555                    expr
1556                }
1557            }
1558            Expression::Var(v) => {
1559                if let Some(canonical) = Self::map_date_part(&v.this) {
1560                    Expression::Identifier(crate::expressions::Identifier {
1561                        name: canonical.to_string(),
1562                        quoted: false,
1563                        trailing_comments: Vec::new(),
1564                        span: None,
1565                    })
1566                } else {
1567                    expr
1568                }
1569            }
1570            Expression::Column(col) if col.table.is_none() => {
1571                if let Some(canonical) = Self::map_date_part(&col.name.name) {
1572                    Expression::Identifier(crate::expressions::Identifier {
1573                        name: canonical.to_string(),
1574                        quoted: false,
1575                        trailing_comments: Vec::new(),
1576                        span: None,
1577                    })
1578                } else {
1579                    expr
1580                }
1581            }
1582            _ => expr,
1583        }
1584    }
1585
1586    /// Transform JSON path for Snowflake GET_PATH function
1587    /// - Convert colon notation to dot notation (y[0]:z -> y[0].z)
1588    /// - Wrap unsafe keys in brackets ($id -> ["$id"])
1589    fn transform_json_path(path: &str) -> String {
1590        // Check if path is just a single key that needs bracket wrapping
1591        // A safe identifier is alphanumeric + underscore, starting with letter/underscore
1592        fn is_safe_identifier(s: &str) -> bool {
1593            if s.is_empty() {
1594                return false;
1595            }
1596            let mut chars = s.chars();
1597            match chars.next() {
1598                Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
1599                _ => return false,
1600            }
1601            chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
1602        }
1603
1604        // Simple path: just a key like "$id" or "field"
1605        // If no dots, brackets, or colons, it's a simple key
1606        if !path.contains('.') && !path.contains('[') && !path.contains(':') {
1607            if is_safe_identifier(path) {
1608                return path.to_string();
1609            } else {
1610                // Wrap unsafe key in bracket notation
1611                return format!("[\"{}\"]", path);
1612            }
1613        }
1614
1615        // Complex path: replace colons with dots
1616        // e.g., y[0]:z -> y[0].z
1617        let result = path.replace(':', ".");
1618        result
1619    }
1620
1621    /// Transform interval to expand abbreviated units (e.g., 'w' -> 'WEEK')
1622    fn transform_interval(&self, interval: crate::expressions::Interval) -> Result<Expression> {
1623        use crate::expressions::{Interval, Literal};
1624
1625        // Unit abbreviation mapping (from Python SQLGlot UNABBREVIATED_UNIT_NAME)
1626        fn expand_unit(abbr: &str) -> &'static str {
1627            match abbr.to_uppercase().as_str() {
1628                "D" => "DAY",
1629                "H" => "HOUR",
1630                "M" => "MINUTE",
1631                "MS" => "MILLISECOND",
1632                "NS" => "NANOSECOND",
1633                "Q" => "QUARTER",
1634                "S" => "SECOND",
1635                "US" => "MICROSECOND",
1636                "W" => "WEEK",
1637                "Y" => "YEAR",
1638                // Full forms (normalize to singular, uppercase)
1639                "WEEK" | "WEEKS" => "WEEK",
1640                "DAY" | "DAYS" => "DAY",
1641                "HOUR" | "HOURS" => "HOUR",
1642                "MINUTE" | "MINUTES" => "MINUTE",
1643                "SECOND" | "SECONDS" => "SECOND",
1644                "MONTH" | "MONTHS" => "MONTH",
1645                "YEAR" | "YEARS" => "YEAR",
1646                "QUARTER" | "QUARTERS" => "QUARTER",
1647                "MILLISECOND" | "MILLISECONDS" => "MILLISECOND",
1648                "MICROSECOND" | "MICROSECONDS" => "MICROSECOND",
1649                "NANOSECOND" | "NANOSECONDS" => "NANOSECOND",
1650                _ => "", // Unknown unit, return empty to indicate no match
1651            }
1652        }
1653
1654        /// Parse an interval string like "1 w" into (value, unit)
1655        fn parse_interval_string(s: &str) -> Option<(&str, &str)> {
1656            let s = s.trim();
1657
1658            // Find where the number ends and the unit begins
1659            // Number can be: optional -, digits, optional decimal point, more digits
1660            let mut num_end = 0;
1661            let mut chars = s.chars().peekable();
1662
1663            // Skip leading minus
1664            if chars.peek() == Some(&'-') {
1665                chars.next();
1666                num_end += 1;
1667            }
1668
1669            // Skip digits
1670            while let Some(&c) = chars.peek() {
1671                if c.is_ascii_digit() {
1672                    chars.next();
1673                    num_end += 1;
1674                } else {
1675                    break;
1676                }
1677            }
1678
1679            // Skip optional decimal point and more digits
1680            if chars.peek() == Some(&'.') {
1681                chars.next();
1682                num_end += 1;
1683                while let Some(&c) = chars.peek() {
1684                    if c.is_ascii_digit() {
1685                        chars.next();
1686                        num_end += 1;
1687                    } else {
1688                        break;
1689                    }
1690                }
1691            }
1692
1693            if num_end == 0 || (num_end == 1 && s.starts_with('-')) {
1694                return None; // No number found
1695            }
1696
1697            let value = &s[..num_end];
1698            let rest = s[num_end..].trim();
1699
1700            // Rest should be alphabetic (the unit)
1701            if rest.is_empty() || !rest.chars().all(|c| c.is_ascii_alphabetic()) {
1702                return None;
1703            }
1704
1705            Some((value, rest))
1706        }
1707
1708        // Check if the interval value is a string literal with embedded value+unit
1709        if let Some(Expression::Literal(ref lit)) = interval.this {
1710            if let Literal::String(ref s) = lit.as_ref() {
1711                if let Some((value, unit)) = parse_interval_string(s) {
1712                    let expanded = expand_unit(unit);
1713                    if !expanded.is_empty() {
1714                        // Construct new string with expanded unit
1715                        let new_value = format!("{} {}", value, expanded);
1716
1717                        return Ok(Expression::Interval(Box::new(Interval {
1718                            this: Some(Expression::Literal(Box::new(Literal::String(new_value)))),
1719                            unit: None, // Unit is now part of the string (SINGLE_STRING_INTERVAL style)
1720                        })));
1721                    }
1722                }
1723            }
1724        }
1725
1726        // No transformation needed
1727        Ok(Expression::Interval(Box::new(interval)))
1728    }
1729
1730    fn transform_function(&self, f: Function) -> Result<Expression> {
1731        // First, recursively transform all function arguments
1732        let transformed_args: Vec<Expression> = f
1733            .args
1734            .into_iter()
1735            .map(|arg| self.transform_expr(arg))
1736            .collect::<Result<Vec<_>>>()?;
1737
1738        let f = Function {
1739            name: f.name,
1740            args: transformed_args,
1741            distinct: f.distinct,
1742            trailing_comments: f.trailing_comments,
1743            use_bracket_syntax: f.use_bracket_syntax,
1744            no_parens: f.no_parens,
1745            quoted: f.quoted,
1746            span: None,
1747            inferred_type: None,
1748        };
1749
1750        let name_upper = f.name.to_uppercase();
1751        match name_upper.as_str() {
1752            // IFNULL -> COALESCE (standardize to COALESCE)
1753            "IFNULL" if f.args.len() == 2 => Ok(Expression::Coalesce(Box::new(VarArgFunc {
1754                original_name: None,
1755                expressions: f.args,
1756                inferred_type: None,
1757            }))),
1758
1759            // NVL -> COALESCE (both work in Snowflake, but COALESCE is standard per SQLGlot)
1760            "NVL" if f.args.len() == 2 => Ok(Expression::Coalesce(Box::new(VarArgFunc {
1761                original_name: None,
1762                expressions: f.args,
1763                inferred_type: None,
1764            }))),
1765
1766            // NVL2 is native to Snowflake
1767            "NVL2" => Ok(Expression::Function(Box::new(f))),
1768
1769            // GROUP_CONCAT -> LISTAGG in Snowflake
1770            "GROUP_CONCAT" if !f.args.is_empty() => Ok(Expression::Function(Box::new(
1771                Function::new("LISTAGG".to_string(), f.args),
1772            ))),
1773
1774            // STRING_AGG -> LISTAGG in Snowflake
1775            "STRING_AGG" if !f.args.is_empty() => Ok(Expression::Function(Box::new(
1776                Function::new("LISTAGG".to_string(), f.args),
1777            ))),
1778
1779            // SUBSTR -> SUBSTRING (both work in Snowflake)
1780            "SUBSTR" => Ok(Expression::Function(Box::new(Function::new(
1781                "SUBSTRING".to_string(),
1782                f.args,
1783            )))),
1784
1785            // UNNEST -> FLATTEN
1786            "UNNEST" => Ok(Expression::Function(Box::new(Function::new(
1787                "FLATTEN".to_string(),
1788                f.args,
1789            )))),
1790
1791            // EXPLODE -> FLATTEN
1792            "EXPLODE" => Ok(Expression::Function(Box::new(Function::new(
1793                "FLATTEN".to_string(),
1794                f.args,
1795            )))),
1796
1797            // CURRENT_DATE is native
1798            "CURRENT_DATE" => Ok(Expression::CurrentDate(crate::expressions::CurrentDate)),
1799
1800            // NOW -> CURRENT_TIMESTAMP (preserving parens style)
1801            "NOW" => Ok(Expression::Function(Box::new(Function {
1802                name: "CURRENT_TIMESTAMP".to_string(),
1803                args: f.args,
1804                distinct: false,
1805                trailing_comments: Vec::new(),
1806                use_bracket_syntax: false,
1807                no_parens: f.no_parens,
1808                quoted: false,
1809                span: None,
1810                inferred_type: None,
1811            }))),
1812
1813            // GETDATE -> CURRENT_TIMESTAMP (preserving parens style)
1814            "GETDATE" => Ok(Expression::Function(Box::new(Function {
1815                name: "CURRENT_TIMESTAMP".to_string(),
1816                args: f.args,
1817                distinct: false,
1818                trailing_comments: Vec::new(),
1819                use_bracket_syntax: false,
1820                no_parens: f.no_parens,
1821                quoted: false,
1822                span: None,
1823                inferred_type: None,
1824            }))),
1825
1826            // CURRENT_TIMESTAMP - always output with parens in Snowflake
1827            // Note: LOCALTIMESTAMP converts to CURRENT_TIMESTAMP without parens,
1828            // but explicit CURRENT_TIMESTAMP calls should have parens
1829            "CURRENT_TIMESTAMP" if f.args.is_empty() => {
1830                Ok(Expression::Function(Box::new(Function {
1831                    name: "CURRENT_TIMESTAMP".to_string(),
1832                    args: Vec::new(),
1833                    distinct: false,
1834                    trailing_comments: Vec::new(),
1835                    use_bracket_syntax: false,
1836                    no_parens: false, // Always output with parens
1837                    quoted: false,
1838                    span: None,
1839                    inferred_type: None,
1840                })))
1841            }
1842
1843            // TO_DATE with single string arg that looks like a date -> CAST(arg AS DATE)
1844            // Per Python SQLGlot: TO_DATE('2013-04-05') -> CAST('2013-04-05' AS DATE)
1845            // But TO_DATE('12345') stays as is (doesn't look like a date)
1846            "TO_DATE" => {
1847                if f.args.len() == 1 {
1848                    if let Expression::Literal(lit) = &f.args[0] {
1849                        if let crate::expressions::Literal::String(s) = lit.as_ref() {
1850                            // Check if the string looks like a date (contains dashes like 2013-04-05)
1851                            if s.contains('-') && s.len() >= 8 && s.len() <= 12 {
1852                                return Ok(Expression::Cast(Box::new(Cast {
1853                                    this: f.args.into_iter().next().unwrap(),
1854                                    to: crate::expressions::DataType::Date,
1855                                    double_colon_syntax: false,
1856                                    trailing_comments: Vec::new(),
1857                                    format: None,
1858                                    default: None,
1859                                    inferred_type: None,
1860                                })));
1861                            }
1862                        }
1863                    }
1864                }
1865                // Normalize format string (2nd arg) if present
1866                let mut args = f.args;
1867                if args.len() >= 2 {
1868                    args[1] = Self::normalize_format_arg(args[1].clone());
1869                }
1870                Ok(Expression::Function(Box::new(Function::new(
1871                    "TO_DATE".to_string(),
1872                    args,
1873                ))))
1874            }
1875
1876            // TO_TIME with single string arg -> CAST(arg AS TIME)
1877            "TO_TIME" => {
1878                if f.args.len() == 1 {
1879                    if let Expression::Literal(lit) = &f.args[0] {
1880                        if let crate::expressions::Literal::String(_) = lit.as_ref() {
1881                            return Ok(Expression::Cast(Box::new(Cast {
1882                                this: f.args.into_iter().next().unwrap(),
1883                                to: crate::expressions::DataType::Time {
1884                                    precision: None,
1885                                    timezone: false,
1886                                },
1887                                double_colon_syntax: false,
1888                                trailing_comments: Vec::new(),
1889                                format: None,
1890                                default: None,
1891                                inferred_type: None,
1892                            })));
1893                        }
1894                    }
1895                }
1896                // Normalize format string (2nd arg) if present
1897                let mut args = f.args;
1898                if args.len() >= 2 {
1899                    args[1] = Self::normalize_format_arg(args[1].clone());
1900                }
1901                Ok(Expression::Function(Box::new(Function::new(
1902                    "TO_TIME".to_string(),
1903                    args,
1904                ))))
1905            }
1906
1907            // TO_TIMESTAMP: Snowflake has multiple forms:
1908            // 1. TO_TIMESTAMP('datetime_string') -> CAST('...' AS TIMESTAMP)
1909            // 2. TO_TIMESTAMP('epoch_string') -> UnixToTime(epoch_string)
1910            // 3. TO_TIMESTAMP(number) -> UnixToTime(number)
1911            // 4. TO_TIMESTAMP(number, scale) where scale is int -> UnixToTime(number, scale)
1912            // 5. TO_TIMESTAMP(string, format) where format is string -> StrToTime(string, format)
1913            "TO_TIMESTAMP" => {
1914                let args = f.args;
1915                if args.len() == 1 {
1916                    let arg = &args[0];
1917                    match arg {
1918                        Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(s) if Self::looks_like_datetime(s)) =>
1919                        {
1920                            let Literal::String(_) = lit.as_ref() else {
1921                                unreachable!()
1922                            };
1923                            // Case 1: datetime string -> CAST AS TIMESTAMP
1924                            return Ok(Expression::Cast(Box::new(Cast {
1925                                this: args.into_iter().next().unwrap(),
1926                                to: DataType::Timestamp {
1927                                    precision: None,
1928                                    timezone: false,
1929                                },
1930                                double_colon_syntax: false,
1931                                trailing_comments: vec![],
1932                                format: None,
1933                                default: None,
1934                                inferred_type: None,
1935                            })));
1936                        }
1937                        Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(s) if Self::looks_like_epoch(s)) =>
1938                        {
1939                            let Literal::String(_) = lit.as_ref() else {
1940                                unreachable!()
1941                            };
1942                            // Case 2: epoch number as string -> UnixToTime
1943                            return Ok(Expression::UnixToTime(Box::new(
1944                                crate::expressions::UnixToTime {
1945                                    this: Box::new(args.into_iter().next().unwrap()),
1946                                    scale: None,
1947                                    zone: None,
1948                                    hours: None,
1949                                    minutes: None,
1950                                    format: None,
1951                                    target_type: None,
1952                                },
1953                            )));
1954                        }
1955                        Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => {
1956                            // Case 3: number -> UnixToTime
1957                            return Ok(Expression::UnixToTime(Box::new(
1958                                crate::expressions::UnixToTime {
1959                                    this: Box::new(args.into_iter().next().unwrap()),
1960                                    scale: None,
1961                                    zone: None,
1962                                    hours: None,
1963                                    minutes: None,
1964                                    format: None,
1965                                    target_type: None,
1966                                },
1967                            )));
1968                        }
1969                        Expression::Neg(_) => {
1970                            // Case 3: number -> UnixToTime
1971                            return Ok(Expression::UnixToTime(Box::new(
1972                                crate::expressions::UnixToTime {
1973                                    this: Box::new(args.into_iter().next().unwrap()),
1974                                    scale: None,
1975                                    zone: None,
1976                                    hours: None,
1977                                    minutes: None,
1978                                    format: None,
1979                                    target_type: None,
1980                                },
1981                            )));
1982                        }
1983                        _ => {
1984                            // Unknown single arg, keep as function
1985                            return Ok(Expression::Function(Box::new(Function::new(
1986                                "TO_TIMESTAMP".to_string(),
1987                                args,
1988                            ))));
1989                        }
1990                    }
1991                } else if args.len() == 2 {
1992                    let second_arg = &args[1];
1993                    // Check if second arg is an integer (scale) or a format string
1994                    let is_int_scale = match second_arg {
1995                        Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => {
1996                            let Literal::Number(n) = lit.as_ref() else {
1997                                unreachable!()
1998                            };
1999                            n.parse::<i64>().is_ok()
2000                        }
2001                        _ => false,
2002                    };
2003
2004                    if is_int_scale {
2005                        // Case 4: TO_TIMESTAMP(number, scale) -> UnixToTime
2006                        let mut args_iter = args.into_iter();
2007                        let value = args_iter.next().unwrap();
2008                        let scale_expr = args_iter.next().unwrap();
2009                        let scale = if let Expression::Literal(lit) = &scale_expr {
2010                            if let Literal::Number(n) = lit.as_ref() {
2011                                n.parse::<i64>().ok()
2012                            } else {
2013                                None
2014                            }
2015                        } else {
2016                            None
2017                        };
2018                        return Ok(Expression::UnixToTime(Box::new(
2019                            crate::expressions::UnixToTime {
2020                                this: Box::new(value),
2021                                scale,
2022                                zone: None,
2023                                hours: None,
2024                                minutes: None,
2025                                format: None,
2026                                target_type: None,
2027                            },
2028                        )));
2029                    } else {
2030                        // Case 5: TO_TIMESTAMP(string, format) -> StrToTime
2031                        let mut args_iter = args.into_iter();
2032                        let value = args_iter.next().unwrap();
2033                        let format_expr = args_iter.next().unwrap();
2034                        let format_str = match &format_expr {
2035                            Expression::Literal(lit)
2036                                if matches!(lit.as_ref(), Literal::String(_)) =>
2037                            {
2038                                let Literal::String(s) = lit.as_ref() else {
2039                                    unreachable!()
2040                                };
2041                                s.clone()
2042                            }
2043                            _ => {
2044                                // Non-string format, keep as function
2045                                return Ok(Expression::Function(Box::new(Function::new(
2046                                    "TO_TIMESTAMP".to_string(),
2047                                    vec![value, format_expr],
2048                                ))));
2049                            }
2050                        };
2051                        // Normalize Snowflake format to target-neutral
2052                        let normalized_format = Self::normalize_snowflake_format(&format_str);
2053                        return Ok(Expression::StrToTime(Box::new(
2054                            crate::expressions::StrToTime {
2055                                this: Box::new(value),
2056                                format: normalized_format,
2057                                zone: None,
2058                                safe: None,
2059                                target_type: None,
2060                            },
2061                        )));
2062                    }
2063                }
2064                // More than 2 args or other cases, keep as function
2065                Ok(Expression::Function(Box::new(Function::new(
2066                    "TO_TIMESTAMP".to_string(),
2067                    args,
2068                ))))
2069            }
2070
2071            // TO_CHAR is native to Snowflake
2072            "TO_CHAR" => Ok(Expression::Function(Box::new(f))),
2073
2074            // ROUND with named args: ROUND(EXPR => x, SCALE => y, ROUNDING_MODE => z)
2075            // -> ROUND(x, y) or ROUND(x, y, z)
2076            "ROUND"
2077                if f.args
2078                    .iter()
2079                    .any(|a| matches!(a, Expression::NamedArgument(_))) =>
2080            {
2081                let mut expr_val = None;
2082                let mut scale_val = None;
2083                let mut rounding_mode_val = None;
2084                for arg in &f.args {
2085                    if let Expression::NamedArgument(na) = arg {
2086                        match na.name.name.to_uppercase().as_str() {
2087                            "EXPR" => expr_val = Some(na.value.clone()),
2088                            "SCALE" => scale_val = Some(na.value.clone()),
2089                            "ROUNDING_MODE" => rounding_mode_val = Some(na.value.clone()),
2090                            _ => {}
2091                        }
2092                    }
2093                }
2094                if let Some(expr) = expr_val {
2095                    let mut args = vec![expr];
2096                    if let Some(scale) = scale_val {
2097                        args.push(scale);
2098                    }
2099                    if let Some(mode) = rounding_mode_val {
2100                        args.push(mode);
2101                    }
2102                    Ok(Expression::Function(Box::new(Function::new(
2103                        "ROUND".to_string(),
2104                        args,
2105                    ))))
2106                } else {
2107                    Ok(Expression::Function(Box::new(f)))
2108                }
2109            }
2110
2111            // DATE_FORMAT -> TO_CHAR in Snowflake
2112            // Also converts strftime format to Snowflake format and wraps first arg in CAST AS TIMESTAMP
2113            "DATE_FORMAT" => {
2114                let mut args = f.args;
2115                // Wrap first arg in CAST AS TIMESTAMP if it's a string literal
2116                if !args.is_empty() {
2117                    if matches!(&args[0], Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)))
2118                    {
2119                        args[0] = Expression::Cast(Box::new(crate::expressions::Cast {
2120                            this: args[0].clone(),
2121                            to: DataType::Timestamp {
2122                                precision: None,
2123                                timezone: false,
2124                            },
2125                            trailing_comments: Vec::new(),
2126                            double_colon_syntax: false,
2127                            format: None,
2128                            default: None,
2129                            inferred_type: None,
2130                        }));
2131                    }
2132                }
2133                // Convert strftime format to Snowflake format
2134                if args.len() >= 2 {
2135                    if let Expression::Literal(ref lit) = args[1] {
2136                        if let Literal::String(ref fmt) = lit.as_ref() {
2137                            let sf_fmt = strftime_to_snowflake_format(fmt);
2138                            args[1] = Expression::Literal(Box::new(Literal::String(sf_fmt)));
2139                        }
2140                    }
2141                }
2142                Ok(Expression::Function(Box::new(Function::new(
2143                    "TO_CHAR".to_string(),
2144                    args,
2145                ))))
2146            }
2147
2148            // ARRAY -> ARRAY_CONSTRUCT
2149            "ARRAY" => Ok(Expression::Function(Box::new(Function::new(
2150                "ARRAY_CONSTRUCT".to_string(),
2151                f.args,
2152            )))),
2153
2154            // STRUCT -> OBJECT_CONSTRUCT
2155            // Convert STRUCT(value AS name, ...) to OBJECT_CONSTRUCT('name', value, ...)
2156            "STRUCT" => {
2157                let mut oc_args = Vec::new();
2158                for arg in f.args {
2159                    match arg {
2160                        Expression::Alias(a) => {
2161                            // Named field: value AS name -> 'name', value
2162                            oc_args.push(Expression::Literal(Box::new(
2163                                crate::expressions::Literal::String(a.alias.name.clone()),
2164                            )));
2165                            oc_args.push(a.this);
2166                        }
2167                        other => {
2168                            // Unnamed field: just pass through
2169                            oc_args.push(other);
2170                        }
2171                    }
2172                }
2173                Ok(Expression::Function(Box::new(Function::new(
2174                    "OBJECT_CONSTRUCT".to_string(),
2175                    oc_args,
2176                ))))
2177            }
2178
2179            // JSON_EXTRACT -> GET_PATH or GET in Snowflake
2180            "JSON_EXTRACT" => Ok(Expression::Function(Box::new(Function::new(
2181                "GET_PATH".to_string(),
2182                f.args,
2183            )))),
2184
2185            // JSON_EXTRACT_SCALAR -> JSON_EXTRACT_PATH_TEXT
2186            "JSON_EXTRACT_SCALAR" => Ok(Expression::Function(Box::new(Function::new(
2187                "JSON_EXTRACT_PATH_TEXT".to_string(),
2188                f.args,
2189            )))),
2190
2191            // LEN -> LENGTH
2192            "LEN" if f.args.len() == 1 => Ok(Expression::Length(Box::new(UnaryFunc::new(
2193                f.args.into_iter().next().unwrap(),
2194            )))),
2195
2196            // CEILING -> CEIL (both work)
2197            "CEILING" if f.args.len() == 1 => Ok(Expression::Ceil(Box::new(CeilFunc {
2198                this: f.args.into_iter().next().unwrap(),
2199                decimals: None,
2200                to: None,
2201            }))),
2202
2203            // CHARINDEX -> POSITION or CHARINDEX (native)
2204            "CHARINDEX" => Ok(Expression::Function(Box::new(f))),
2205
2206            // SPLIT is native to Snowflake - keep as-is
2207            "SPLIT" => Ok(Expression::Function(Box::new(f))),
2208
2209            // ARRAY_AGG is native to Snowflake
2210            "ARRAY_AGG" => Ok(Expression::Function(Box::new(f))),
2211
2212            // PARSE_JSON for JSON parsing
2213            "JSON_PARSE" | "PARSE_JSON" => Ok(Expression::Function(Box::new(Function::new(
2214                "PARSE_JSON".to_string(),
2215                f.args,
2216            )))),
2217
2218            // RAND -> Rand (to use RANDOM in Snowflake)
2219            "RAND" => {
2220                let seed = f.args.first().cloned().map(Box::new);
2221                Ok(Expression::Rand(Box::new(crate::expressions::Rand {
2222                    seed,
2223                    lower: None,
2224                    upper: None,
2225                })))
2226            }
2227
2228            // SHA -> SHA1
2229            "SHA" => Ok(Expression::Function(Box::new(Function::new(
2230                "SHA1".to_string(),
2231                f.args,
2232            )))),
2233
2234            // APPROX_COUNT_DISTINCT is native
2235            "APPROX_DISTINCT" => Ok(Expression::Function(Box::new(Function::new(
2236                "APPROX_COUNT_DISTINCT".to_string(),
2237                f.args,
2238            )))),
2239
2240            // GEN_RANDOM_UUID/UUID -> Uuid AST node
2241            "GEN_RANDOM_UUID" | "UUID" => {
2242                Ok(Expression::Uuid(Box::new(crate::expressions::Uuid {
2243                    this: None,
2244                    name: None,
2245                    is_string: None,
2246                })))
2247            }
2248
2249            // NEWID -> Uuid AST node
2250            "NEWID" => Ok(Expression::Uuid(Box::new(crate::expressions::Uuid {
2251                this: None,
2252                name: None,
2253                is_string: None,
2254            }))),
2255
2256            // UUID_STRING -> Uuid AST node (without args only; with args keep as Function for identity)
2257            "UUID_STRING" => {
2258                if f.args.is_empty() {
2259                    Ok(Expression::Uuid(Box::new(crate::expressions::Uuid {
2260                        this: None,
2261                        name: None,
2262                        is_string: None,
2263                    })))
2264                } else {
2265                    Ok(Expression::Function(Box::new(Function::new(
2266                        "UUID_STRING".to_string(),
2267                        f.args,
2268                    ))))
2269                }
2270            }
2271
2272            // IF -> IFF (convert to IfFunc AST node)
2273            "IF" if f.args.len() >= 2 => {
2274                let mut args = f.args;
2275                let condition = args.remove(0);
2276                let true_val = args.remove(0);
2277                let false_val = if !args.is_empty() {
2278                    Some(args.remove(0))
2279                } else {
2280                    None
2281                };
2282                Ok(Expression::IfFunc(Box::new(crate::expressions::IfFunc {
2283                    condition,
2284                    true_value: true_val,
2285                    false_value: Some(
2286                        false_val.unwrap_or(Expression::Null(crate::expressions::Null)),
2287                    ),
2288                    original_name: Some("IFF".to_string()),
2289                    inferred_type: None,
2290                })))
2291            }
2292
2293            // SQUARE(x) -> POWER(x, 2)
2294            "SQUARE" if f.args.len() == 1 => {
2295                let x = f.args.into_iter().next().unwrap();
2296                Ok(Expression::Power(Box::new(
2297                    crate::expressions::BinaryFunc {
2298                        original_name: None,
2299                        this: x,
2300                        expression: Expression::number(2),
2301                        inferred_type: None,
2302                    },
2303                )))
2304            }
2305
2306            // POW(x, y) -> POWER(x, y)
2307            "POW" if f.args.len() == 2 => {
2308                let mut args = f.args.into_iter();
2309                let x = args.next().unwrap();
2310                let y = args.next().unwrap();
2311                Ok(Expression::Power(Box::new(
2312                    crate::expressions::BinaryFunc {
2313                        original_name: None,
2314                        this: x,
2315                        expression: y,
2316                        inferred_type: None,
2317                    },
2318                )))
2319            }
2320
2321            // MOD(x, y) -> x % y (modulo operator)
2322            "MOD" if f.args.len() == 2 => {
2323                let mut args = f.args.into_iter();
2324                let x = args.next().unwrap();
2325                let y = args.next().unwrap();
2326                Ok(Expression::Mod(Box::new(crate::expressions::BinaryOp {
2327                    left: x,
2328                    right: y,
2329                    left_comments: Vec::new(),
2330                    operator_comments: Vec::new(),
2331                    trailing_comments: Vec::new(),
2332                    inferred_type: None,
2333                })))
2334            }
2335
2336            // APPROXIMATE_JACCARD_INDEX -> APPROXIMATE_SIMILARITY
2337            "APPROXIMATE_JACCARD_INDEX" => Ok(Expression::Function(Box::new(Function::new(
2338                "APPROXIMATE_SIMILARITY".to_string(),
2339                f.args,
2340            )))),
2341
2342            // ARRAY_CONSTRUCT -> Array with bracket notation in Snowflake
2343            "ARRAY_CONSTRUCT" => Ok(Expression::ArrayFunc(Box::new(
2344                crate::expressions::ArrayConstructor {
2345                    expressions: f.args,
2346                    bracket_notation: true,
2347                    use_list_keyword: false,
2348                },
2349            ))),
2350
2351            // APPROX_TOP_K - add default k=1 if not provided
2352            "APPROX_TOP_K" if f.args.len() == 1 => {
2353                let mut args = f.args;
2354                args.push(Expression::number(1));
2355                Ok(Expression::Function(Box::new(Function::new(
2356                    "APPROX_TOP_K".to_string(),
2357                    args,
2358                ))))
2359            }
2360
2361            // TO_DECIMAL, TO_NUMERIC -> TO_NUMBER
2362            "TO_DECIMAL" | "TO_NUMERIC" => Ok(Expression::Function(Box::new(Function::new(
2363                "TO_NUMBER".to_string(),
2364                f.args,
2365            )))),
2366
2367            // TRY_TO_DECIMAL, TRY_TO_NUMERIC -> TRY_TO_NUMBER
2368            "TRY_TO_DECIMAL" | "TRY_TO_NUMERIC" => Ok(Expression::Function(Box::new(
2369                Function::new("TRY_TO_NUMBER".to_string(), f.args),
2370            ))),
2371
2372            // STDDEV_SAMP -> STDDEV
2373            "STDDEV_SAMP" => Ok(Expression::Function(Box::new(Function::new(
2374                "STDDEV".to_string(),
2375                f.args,
2376            )))),
2377
2378            // STRTOK -> SPLIT_PART (with default delimiter and position)
2379            "STRTOK" if f.args.len() >= 1 => {
2380                let mut args = f.args;
2381                // Add default delimiter (space) if missing
2382                if args.len() == 1 {
2383                    args.push(Expression::string(" ".to_string()));
2384                }
2385                // Add default position (1) if missing
2386                if args.len() == 2 {
2387                    args.push(Expression::number(1));
2388                }
2389                Ok(Expression::Function(Box::new(Function::new(
2390                    "STRTOK".to_string(),
2391                    args,
2392                ))))
2393            }
2394
2395            "STRTOK_TO_ARRAY" if f.args.len() == 1 => {
2396                let mut args = f.args;
2397                args.push(Expression::string(" ".to_string()));
2398                Ok(Expression::Function(Box::new(Function::new(
2399                    "STRTOK_TO_ARRAY".to_string(),
2400                    args,
2401                ))))
2402            }
2403
2404            // WEEKOFYEAR -> WEEK
2405            "WEEKOFYEAR" => Ok(Expression::Function(Box::new(Function::new(
2406                "WEEK".to_string(),
2407                f.args,
2408            )))),
2409
2410            // LIKE(col, pattern, escape) -> col LIKE pattern ESCAPE escape
2411            "LIKE" if f.args.len() >= 2 => {
2412                let mut args = f.args.into_iter();
2413                let left = args.next().unwrap();
2414                let right = args.next().unwrap();
2415                let escape = args.next();
2416                Ok(Expression::Like(Box::new(crate::expressions::LikeOp {
2417                    left,
2418                    right,
2419                    escape,
2420                    quantifier: None,
2421                    inferred_type: None,
2422                })))
2423            }
2424
2425            // ILIKE(col, pattern, escape) -> col ILIKE pattern ESCAPE escape
2426            "ILIKE" if f.args.len() >= 2 => {
2427                let mut args = f.args.into_iter();
2428                let left = args.next().unwrap();
2429                let right = args.next().unwrap();
2430                let escape = args.next();
2431                Ok(Expression::ILike(Box::new(crate::expressions::LikeOp {
2432                    left,
2433                    right,
2434                    escape,
2435                    quantifier: None,
2436                    inferred_type: None,
2437                })))
2438            }
2439
2440            // RLIKE -> REGEXP_LIKE
2441            "RLIKE" if f.args.len() >= 2 => {
2442                let mut args = f.args.into_iter();
2443                let left = args.next().unwrap();
2444                let pattern = args.next().unwrap();
2445                let flags = args.next();
2446                Ok(Expression::RegexpLike(Box::new(
2447                    crate::expressions::RegexpFunc {
2448                        this: left,
2449                        pattern,
2450                        flags,
2451                    },
2452                )))
2453            }
2454
2455            // IFF -> convert to IfFunc AST node for proper cross-dialect handling
2456            "IFF" if f.args.len() >= 2 => {
2457                let mut args = f.args;
2458                let condition = args.remove(0);
2459                let true_value = args.remove(0);
2460                let false_value = if !args.is_empty() {
2461                    Some(args.remove(0))
2462                } else {
2463                    None
2464                };
2465                Ok(Expression::IfFunc(Box::new(crate::expressions::IfFunc {
2466                    condition,
2467                    true_value,
2468                    false_value,
2469                    original_name: Some("IFF".to_string()),
2470                    inferred_type: None,
2471                })))
2472            }
2473
2474            // TIMESTAMP_NTZ_FROM_PARTS, TIMESTAMPFROMPARTS, TIMESTAMPNTZFROMPARTS -> TIMESTAMP_FROM_PARTS
2475            "TIMESTAMP_NTZ_FROM_PARTS" | "TIMESTAMPFROMPARTS" | "TIMESTAMPNTZFROMPARTS" => {
2476                Ok(Expression::Function(Box::new(Function::new(
2477                    "TIMESTAMP_FROM_PARTS".to_string(),
2478                    f.args,
2479                ))))
2480            }
2481
2482            // TIMESTAMPLTZFROMPARTS -> TIMESTAMP_LTZ_FROM_PARTS
2483            "TIMESTAMPLTZFROMPARTS" => Ok(Expression::Function(Box::new(Function::new(
2484                "TIMESTAMP_LTZ_FROM_PARTS".to_string(),
2485                f.args,
2486            )))),
2487
2488            // TIMESTAMPTZFROMPARTS -> TIMESTAMP_TZ_FROM_PARTS
2489            "TIMESTAMPTZFROMPARTS" => Ok(Expression::Function(Box::new(Function::new(
2490                "TIMESTAMP_TZ_FROM_PARTS".to_string(),
2491                f.args,
2492            )))),
2493
2494            // DATEADD with 3 args - transform the unit (first arg) using date part mapping
2495            "DATEADD" if f.args.len() >= 1 => {
2496                let mut args = f.args;
2497                args[0] = self.transform_date_part_arg(args[0].clone());
2498                Ok(Expression::Function(Box::new(Function::new(
2499                    "DATEADD".to_string(),
2500                    args,
2501                ))))
2502            }
2503
2504            // DATEDIFF with 3 args - transform the unit (first arg) using date part mapping
2505            // Also convert _POLYGLOT_TO_DATE back to TO_DATE (from cross-dialect normalize)
2506            "DATEDIFF" if f.args.len() >= 1 => {
2507                let mut args = f.args;
2508                args[0] = self.transform_date_part_arg(args[0].clone());
2509                // Convert _POLYGLOT_TO_DATE back to TO_DATE for date args
2510                // (_POLYGLOT_TO_DATE is an internal marker from cross-dialect normalize)
2511                for i in 1..args.len() {
2512                    if let Expression::Function(ref func) = args[i] {
2513                        if func.name == "_POLYGLOT_TO_DATE" {
2514                            let inner_args = func.args.clone();
2515                            args[i] = Expression::Function(Box::new(Function::new(
2516                                "TO_DATE".to_string(),
2517                                inner_args,
2518                            )));
2519                        }
2520                    }
2521                }
2522                Ok(Expression::Function(Box::new(Function::new(
2523                    "DATEDIFF".to_string(),
2524                    args,
2525                ))))
2526            }
2527
2528            // TIMEDIFF -> DATEDIFF
2529            "TIMEDIFF" => Ok(Expression::Function(Box::new(Function::new(
2530                "DATEDIFF".to_string(),
2531                f.args,
2532            )))),
2533
2534            // TIMESTAMPDIFF -> DATEDIFF
2535            "TIMESTAMPDIFF" => Ok(Expression::Function(Box::new(Function::new(
2536                "DATEDIFF".to_string(),
2537                f.args,
2538            )))),
2539
2540            // TIMESTAMPADD -> DATEADD
2541            "TIMESTAMPADD" => Ok(Expression::Function(Box::new(Function::new(
2542                "DATEADD".to_string(),
2543                f.args,
2544            )))),
2545
2546            // TIMEADD -> preserve it
2547            "TIMEADD" => Ok(Expression::Function(Box::new(f))),
2548
2549            // DATE_FROM_PARTS, DATEFROMPARTS -> DATE_FROM_PARTS
2550            "DATEFROMPARTS" => Ok(Expression::Function(Box::new(Function::new(
2551                "DATE_FROM_PARTS".to_string(),
2552                f.args,
2553            )))),
2554
2555            // TIME_FROM_PARTS, TIMEFROMPARTS -> TIME_FROM_PARTS
2556            "TIMEFROMPARTS" => Ok(Expression::Function(Box::new(Function::new(
2557                "TIME_FROM_PARTS".to_string(),
2558                f.args,
2559            )))),
2560
2561            // DAYOFWEEK -> DAYOFWEEK (preserve)
2562            "DAYOFWEEK" => Ok(Expression::Function(Box::new(f))),
2563
2564            // DAYOFMONTH -> DAYOFMONTH (preserve)
2565            "DAYOFMONTH" => Ok(Expression::Function(Box::new(f))),
2566
2567            // DAYOFYEAR -> DAYOFYEAR (preserve)
2568            "DAYOFYEAR" => Ok(Expression::Function(Box::new(f))),
2569
2570            // MONTHNAME -> Monthname AST node (abbreviated=true for Snowflake)
2571            // Target dialects can then convert to their native form
2572            "MONTHNAME" if f.args.len() == 1 => {
2573                let arg = f.args.into_iter().next().unwrap();
2574                Ok(Expression::Monthname(Box::new(
2575                    crate::expressions::Monthname {
2576                        this: Box::new(arg),
2577                        abbreviated: Some(Box::new(Expression::Literal(Box::new(
2578                            Literal::String("true".to_string()),
2579                        )))),
2580                    },
2581                )))
2582            }
2583
2584            // DAYNAME -> Dayname AST node (abbreviated=true for Snowflake)
2585            // Target dialects can then convert to their native form
2586            "DAYNAME" if f.args.len() == 1 => {
2587                let arg = f.args.into_iter().next().unwrap();
2588                Ok(Expression::Dayname(Box::new(crate::expressions::Dayname {
2589                    this: Box::new(arg),
2590                    abbreviated: Some(Box::new(Expression::Literal(Box::new(Literal::String(
2591                        "true".to_string(),
2592                    ))))),
2593                })))
2594            }
2595
2596            // BOOLAND_AGG/BOOL_AND/LOGICAL_AND -> LogicalAnd AST node
2597            "BOOLAND_AGG" | "BOOL_AND" | "LOGICAL_AND" if !f.args.is_empty() => {
2598                let arg = f.args.into_iter().next().unwrap();
2599                Ok(Expression::LogicalAnd(Box::new(AggFunc {
2600                    this: arg,
2601                    distinct: false,
2602                    filter: None,
2603                    order_by: Vec::new(),
2604                    name: Some("BOOLAND_AGG".to_string()),
2605                    ignore_nulls: None,
2606                    having_max: None,
2607                    limit: None,
2608                    inferred_type: None,
2609                })))
2610            }
2611
2612            // BOOLOR_AGG/BOOL_OR/LOGICAL_OR -> LogicalOr AST node
2613            "BOOLOR_AGG" | "BOOL_OR" | "LOGICAL_OR" if !f.args.is_empty() => {
2614                let arg = f.args.into_iter().next().unwrap();
2615                Ok(Expression::LogicalOr(Box::new(AggFunc {
2616                    this: arg,
2617                    distinct: false,
2618                    filter: None,
2619                    order_by: Vec::new(),
2620                    name: Some("BOOLOR_AGG".to_string()),
2621                    ignore_nulls: None,
2622                    having_max: None,
2623                    limit: None,
2624                    inferred_type: None,
2625                })))
2626            }
2627
2628            // SKEW -> Skewness AST node for proper cross-dialect handling
2629            "SKEW" | "SKEWNESS" if !f.args.is_empty() => {
2630                let arg = f.args.into_iter().next().unwrap();
2631                Ok(Expression::Skewness(Box::new(AggFunc {
2632                    this: arg,
2633                    distinct: false,
2634                    filter: None,
2635                    order_by: Vec::new(),
2636                    name: Some("SKEW".to_string()),
2637                    ignore_nulls: None,
2638                    having_max: None,
2639                    limit: None,
2640                    inferred_type: None,
2641                })))
2642            }
2643
2644            // VAR_SAMP -> VARIANCE (Snowflake uses VARIANCE for sample variance)
2645            "VAR_SAMP" => Ok(Expression::Function(Box::new(Function::new(
2646                "VARIANCE".to_string(),
2647                f.args,
2648            )))),
2649
2650            // VAR_POP -> VARIANCE_POP
2651            "VAR_POP" => Ok(Expression::Function(Box::new(Function::new(
2652                "VARIANCE_POP".to_string(),
2653                f.args,
2654            )))),
2655
2656            // DATE(str) -> TO_DATE(str) (single-arg form)
2657            "DATE" if f.args.len() == 1 => Ok(Expression::Function(Box::new(Function::new(
2658                "TO_DATE".to_string(),
2659                f.args,
2660            )))),
2661            // DATE(str, format) -> TO_DATE(str, normalized_format)
2662            // Python SQLGlot normalizes DATE(...) to TO_DATE(...) for formatted variants.
2663            // But _POLYGLOT_DATE(str, format) stays as DATE() (from BigQuery PARSE_DATE conversion)
2664            "DATE" if f.args.len() >= 2 => {
2665                let mut args = f.args;
2666                args[1] = Self::normalize_format_arg(args[1].clone());
2667                Ok(Expression::Function(Box::new(Function::new(
2668                    "TO_DATE".to_string(),
2669                    args,
2670                ))))
2671            }
2672            // Internal marker from BigQuery PARSE_DATE -> Snowflake conversion
2673            // _POLYGLOT_DATE stays as DATE() (not converted to TO_DATE)
2674            "_POLYGLOT_DATE" if f.args.len() >= 2 => {
2675                let mut args = f.args;
2676                args[1] = Self::normalize_format_arg(args[1].clone());
2677                Ok(Expression::Function(Box::new(Function::new(
2678                    "DATE".to_string(),
2679                    args,
2680                ))))
2681            }
2682
2683            // DESCRIBE/DESC normalization
2684            "DESCRIBE" => Ok(Expression::Function(Box::new(f))),
2685
2686            // MD5 -> MD5 (preserve) but MD5_HEX -> MD5
2687            "MD5_HEX" => Ok(Expression::Function(Box::new(Function::new(
2688                "MD5".to_string(),
2689                f.args,
2690            )))),
2691
2692            // SHA1_HEX -> SHA1
2693            "SHA1_HEX" => Ok(Expression::Function(Box::new(Function::new(
2694                "SHA1".to_string(),
2695                f.args,
2696            )))),
2697
2698            // SHA2_HEX -> SHA2
2699            "SHA2_HEX" => Ok(Expression::Function(Box::new(Function::new(
2700                "SHA2".to_string(),
2701                f.args,
2702            )))),
2703
2704            // EDITDISTANCE -> EDITDISTANCE (preserve Snowflake name)
2705            "LEVENSHTEIN" => Ok(Expression::Function(Box::new(Function::new(
2706                "EDITDISTANCE".to_string(),
2707                f.args,
2708            )))),
2709
2710            // BIT_NOT -> BITNOT
2711            "BIT_NOT" if f.args.len() == 1 => Ok(Expression::Function(Box::new(Function::new(
2712                "BITNOT".to_string(),
2713                f.args,
2714            )))),
2715
2716            // BIT_AND -> BITAND
2717            "BIT_AND" if f.args.len() >= 2 => Ok(Expression::Function(Box::new(Function::new(
2718                "BITAND".to_string(),
2719                f.args,
2720            )))),
2721
2722            // BIT_OR -> BITOR
2723            "BIT_OR" if f.args.len() >= 2 => Ok(Expression::Function(Box::new(Function::new(
2724                "BITOR".to_string(),
2725                f.args,
2726            )))),
2727
2728            // BIT_XOR -> BITXOR
2729            "BIT_XOR" if f.args.len() >= 2 => Ok(Expression::Function(Box::new(Function::new(
2730                "BITXOR".to_string(),
2731                f.args,
2732            )))),
2733
2734            // BIT_SHIFTLEFT -> BITSHIFTLEFT
2735            "BIT_SHIFTLEFT" if f.args.len() >= 2 => Ok(Expression::Function(Box::new(
2736                Function::new("BITSHIFTLEFT".to_string(), f.args),
2737            ))),
2738
2739            // BIT_SHIFTRIGHT -> BITSHIFTRIGHT
2740            "BIT_SHIFTRIGHT" if f.args.len() >= 2 => Ok(Expression::Function(Box::new(
2741                Function::new("BITSHIFTRIGHT".to_string(), f.args),
2742            ))),
2743
2744            // SYSTIMESTAMP -> CURRENT_TIMESTAMP (preserving parens style)
2745            "SYSTIMESTAMP" => Ok(Expression::Function(Box::new(Function {
2746                name: "CURRENT_TIMESTAMP".to_string(),
2747                args: f.args,
2748                distinct: false,
2749                trailing_comments: Vec::new(),
2750                use_bracket_syntax: false,
2751                no_parens: f.no_parens,
2752                quoted: false,
2753                span: None,
2754                inferred_type: None,
2755            }))),
2756
2757            // LOCALTIMESTAMP -> CURRENT_TIMESTAMP (preserving parens style)
2758            "LOCALTIMESTAMP" => Ok(Expression::Function(Box::new(Function {
2759                name: "CURRENT_TIMESTAMP".to_string(),
2760                args: f.args,
2761                distinct: false,
2762                trailing_comments: Vec::new(),
2763                use_bracket_syntax: false,
2764                no_parens: f.no_parens,
2765                quoted: false,
2766                span: None,
2767                inferred_type: None,
2768            }))),
2769
2770            // SPACE(n) -> REPEAT(' ', n) in Snowflake
2771            "SPACE" if f.args.len() == 1 => {
2772                let arg = f.args.into_iter().next().unwrap();
2773                Ok(Expression::Function(Box::new(Function::new(
2774                    "REPEAT".to_string(),
2775                    vec![
2776                        Expression::Literal(Box::new(Literal::String(" ".to_string()))),
2777                        arg,
2778                    ],
2779                ))))
2780            }
2781
2782            // CEILING -> CEIL
2783            "CEILING" => Ok(Expression::Function(Box::new(Function::new(
2784                "CEIL".to_string(),
2785                f.args,
2786            )))),
2787
2788            // LOG without base -> LN
2789            "LOG" if f.args.len() == 1 => Ok(Expression::Function(Box::new(Function::new(
2790                "LN".to_string(),
2791                f.args,
2792            )))),
2793
2794            // REGEXP_SUBSTR_ALL is native to Snowflake
2795            "REGEXP_SUBSTR_ALL" => Ok(Expression::Function(Box::new(f))),
2796
2797            // GET_PATH - transform path argument:
2798            // - Convert colon notation to dot notation (y[0]:z -> y[0].z)
2799            // - Wrap unsafe keys in brackets ($id -> ["$id"])
2800            "GET_PATH" if f.args.len() >= 2 => {
2801                let mut args = f.args;
2802                // Transform the path argument (second argument)
2803                if let Expression::Literal(lit) = &args[1] {
2804                    if let crate::expressions::Literal::String(path) = lit.as_ref() {
2805                        let transformed = Self::transform_json_path(path);
2806                        args[1] = Expression::Literal(Box::new(
2807                            crate::expressions::Literal::String(transformed),
2808                        ));
2809                    }
2810                }
2811                Ok(Expression::Function(Box::new(Function::new(
2812                    "GET_PATH".to_string(),
2813                    args,
2814                ))))
2815            }
2816            "GET_PATH" => Ok(Expression::Function(Box::new(f))),
2817
2818            // FLATTEN is native to Snowflake
2819            "FLATTEN" => Ok(Expression::Function(Box::new(f))),
2820
2821            // DATE_TRUNC - transform unit to quoted string
2822            // DATE_TRUNC(yr, x) -> DATE_TRUNC('YEAR', x)
2823            "DATE_TRUNC" if f.args.len() >= 1 => {
2824                let mut args = f.args;
2825                // Transform the unit to canonical form and convert to string literal
2826                let unit_name = match &args[0] {
2827                    Expression::Identifier(id) => Some(id.name.as_str()),
2828                    Expression::Var(v) => Some(v.this.as_str()),
2829                    Expression::Column(col) if col.table.is_none() => Some(col.name.name.as_str()),
2830                    _ => None,
2831                };
2832                if let Some(name) = unit_name {
2833                    let canonical = Self::map_date_part(name).unwrap_or(name);
2834                    args[0] = Expression::Literal(Box::new(crate::expressions::Literal::String(
2835                        canonical.to_uppercase(),
2836                    )));
2837                }
2838                Ok(Expression::Function(Box::new(Function::new(
2839                    "DATE_TRUNC".to_string(),
2840                    args,
2841                ))))
2842            }
2843
2844            // DATE_PART - transform unit argument
2845            // DATE_PART(yyy, x) -> DATE_PART(YEAR, x)
2846            // Only convert string literals to identifiers when the second arg is a typed literal
2847            // (e.g., TIMESTAMP '...', DATE '...'), indicating the function came from another dialect.
2848            // For native Snowflake DATE_PART('month', CAST(...)), preserve the string as-is.
2849            "DATE_PART" if f.args.len() >= 1 => {
2850                let mut args = f.args;
2851                let from_typed_literal = args.len() >= 2
2852                    && matches!(
2853                        &args[1],
2854                        Expression::Literal(lit) if matches!(lit.as_ref(),
2855                            crate::expressions::Literal::Timestamp(_)
2856                            | crate::expressions::Literal::Date(_)
2857                            | crate::expressions::Literal::Time(_)
2858                            | crate::expressions::Literal::Datetime(_)
2859                        )
2860                    );
2861                if from_typed_literal {
2862                    args[0] = self.transform_date_part_arg(args[0].clone());
2863                } else {
2864                    // For non-typed-literal cases, only normalize identifiers/columns
2865                    // (don't convert string literals to identifiers)
2866                    args[0] = self.transform_date_part_arg_identifiers_only(args[0].clone());
2867                }
2868                Ok(Expression::Function(Box::new(Function::new(
2869                    "DATE_PART".to_string(),
2870                    args,
2871                ))))
2872            }
2873
2874            // OBJECT_CONSTRUCT is native to Snowflake
2875            "OBJECT_CONSTRUCT" => Ok(Expression::Function(Box::new(f))),
2876
2877            // OBJECT_CONSTRUCT_KEEP_NULL is native to Snowflake
2878            "OBJECT_CONSTRUCT_KEEP_NULL" => Ok(Expression::Function(Box::new(f))),
2879
2880            // DESC -> DESCRIBE
2881            "DESC" => Ok(Expression::Function(Box::new(Function::new(
2882                "DESCRIBE".to_string(),
2883                f.args,
2884            )))),
2885
2886            // RLIKE -> REGEXP_LIKE
2887            "RLIKE" if f.args.len() >= 2 => Ok(Expression::Function(Box::new(Function::new(
2888                "REGEXP_LIKE".to_string(),
2889                f.args,
2890            )))),
2891
2892            // TRANSFORM function - handle typed lambda parameters
2893            // For typed lambdas like `a int -> a + 1`, we need to:
2894            // 1. Remove the type annotation from the parameter
2895            // 2. Wrap all references to the parameter in the body with CAST(param AS type)
2896            "TRANSFORM" => {
2897                let transformed_args: Vec<Expression> = f
2898                    .args
2899                    .into_iter()
2900                    .map(|arg| {
2901                        if let Expression::Lambda(lambda) = arg {
2902                            self.transform_typed_lambda(*lambda)
2903                        } else {
2904                            arg
2905                        }
2906                    })
2907                    .collect();
2908                Ok(Expression::Function(Box::new(Function::new(
2909                    "TRANSFORM".to_string(),
2910                    transformed_args,
2911                ))))
2912            }
2913
2914            // SEARCH function - convert to Search expression with canonical parameter ordering
2915            "SEARCH" if f.args.len() >= 2 => {
2916                let mut args = f.args.into_iter();
2917                let this = Box::new(args.next().unwrap());
2918                let expression = Box::new(args.next().unwrap());
2919
2920                let mut analyzer: Option<Box<Expression>> = None;
2921                let mut search_mode: Option<Box<Expression>> = None;
2922
2923                // Parse remaining named arguments
2924                for arg in args {
2925                    if let Expression::NamedArgument(na) = &arg {
2926                        let name_upper = na.name.name.to_uppercase();
2927                        match name_upper.as_str() {
2928                            "ANALYZER" => analyzer = Some(Box::new(arg)),
2929                            "SEARCH_MODE" => search_mode = Some(Box::new(arg)),
2930                            _ => {}
2931                        }
2932                    }
2933                }
2934
2935                Ok(Expression::Search(Box::new(crate::expressions::Search {
2936                    this,
2937                    expression,
2938                    json_scope: None,
2939                    analyzer,
2940                    analyzer_options: None,
2941                    search_mode,
2942                })))
2943            }
2944
2945            // ODBC CONVERT function: CONVERT(value, SQL_TYPE) -> CAST(value AS TYPE)
2946            // This handles the { fn CONVERT(...) } ODBC escape sequence syntax
2947            "CONVERT" if f.args.len() == 2 => {
2948                let value = f.args.get(0).cloned().unwrap();
2949                let type_arg = f.args.get(1).cloned().unwrap();
2950
2951                // Check if second argument is a SQL_ type identifier
2952                if let Expression::Column(col) = &type_arg {
2953                    let type_name = col.name.name.to_uppercase();
2954                    let data_type = match type_name.as_str() {
2955                        "SQL_DOUBLE" => Some(DataType::Double {
2956                            precision: None,
2957                            scale: None,
2958                        }),
2959                        "SQL_VARCHAR" => Some(DataType::VarChar {
2960                            length: None,
2961                            parenthesized_length: false,
2962                        }),
2963                        "SQL_INTEGER" | "SQL_INT" => Some(DataType::Int {
2964                            length: None,
2965                            integer_spelling: false,
2966                        }),
2967                        "SQL_BIGINT" => Some(DataType::BigInt { length: None }),
2968                        "SQL_SMALLINT" => Some(DataType::SmallInt { length: None }),
2969                        "SQL_FLOAT" => Some(DataType::Float {
2970                            precision: None,
2971                            scale: None,
2972                            real_spelling: false,
2973                        }),
2974                        "SQL_REAL" => Some(DataType::Float {
2975                            precision: None,
2976                            scale: None,
2977                            real_spelling: true,
2978                        }),
2979                        "SQL_DECIMAL" => Some(DataType::Decimal {
2980                            precision: None,
2981                            scale: None,
2982                        }),
2983                        "SQL_DATE" => Some(DataType::Date),
2984                        "SQL_TIME" => Some(DataType::Time {
2985                            precision: None,
2986                            timezone: false,
2987                        }),
2988                        "SQL_TIMESTAMP" => Some(DataType::Timestamp {
2989                            precision: None,
2990                            timezone: false,
2991                        }),
2992                        _ => None,
2993                    };
2994
2995                    if let Some(dt) = data_type {
2996                        return Ok(Expression::Cast(Box::new(Cast {
2997                            this: value,
2998                            to: dt,
2999                            double_colon_syntax: false,
3000                            trailing_comments: vec![],
3001                            format: None,
3002                            default: None,
3003                            inferred_type: None,
3004                        })));
3005                    }
3006                }
3007                // If not a SQL_ type, keep as regular CONVERT function
3008                Ok(Expression::Function(Box::new(f)))
3009            }
3010
3011            // TO_TIMESTAMP_TZ: single string arg -> CAST(... AS TIMESTAMPTZ), otherwise keep as function
3012            // Per Python sqlglot: _build_datetime converts TO_TIMESTAMP_TZ('string') to CAST('string' AS TIMESTAMPTZ)
3013            "TO_TIMESTAMP_TZ" => {
3014                if f.args.len() == 1 {
3015                    if let Expression::Literal(lit) = &f.args[0] {
3016                        if let crate::expressions::Literal::String(_) = lit.as_ref() {
3017                            return Ok(Expression::Cast(Box::new(Cast {
3018                                this: f.args.into_iter().next().unwrap(),
3019                                to: DataType::Custom {
3020                                    name: "TIMESTAMPTZ".to_string(),
3021                                },
3022                                double_colon_syntax: false,
3023                                trailing_comments: vec![],
3024                                format: None,
3025                                default: None,
3026                                inferred_type: None,
3027                            })));
3028                        }
3029                    }
3030                }
3031                Ok(Expression::Function(Box::new(f)))
3032            }
3033
3034            // TO_TIMESTAMP_NTZ: single string arg -> CAST(... AS TIMESTAMPNTZ), otherwise keep as function
3035            "TO_TIMESTAMP_NTZ" => {
3036                if f.args.len() == 1 {
3037                    if let Expression::Literal(lit) = &f.args[0] {
3038                        if let crate::expressions::Literal::String(_) = lit.as_ref() {
3039                            return Ok(Expression::Cast(Box::new(Cast {
3040                                this: f.args.into_iter().next().unwrap(),
3041                                to: DataType::Custom {
3042                                    name: "TIMESTAMPNTZ".to_string(),
3043                                },
3044                                double_colon_syntax: false,
3045                                trailing_comments: vec![],
3046                                format: None,
3047                                default: None,
3048                                inferred_type: None,
3049                            })));
3050                        }
3051                    }
3052                }
3053                Ok(Expression::Function(Box::new(f)))
3054            }
3055
3056            // TO_TIMESTAMP_LTZ: single string arg -> CAST(... AS TIMESTAMPLTZ), otherwise keep as function
3057            "TO_TIMESTAMP_LTZ" => {
3058                if f.args.len() == 1 {
3059                    if let Expression::Literal(lit) = &f.args[0] {
3060                        if let crate::expressions::Literal::String(_) = lit.as_ref() {
3061                            return Ok(Expression::Cast(Box::new(Cast {
3062                                this: f.args.into_iter().next().unwrap(),
3063                                to: DataType::Custom {
3064                                    name: "TIMESTAMPLTZ".to_string(),
3065                                },
3066                                double_colon_syntax: false,
3067                                trailing_comments: vec![],
3068                                format: None,
3069                                default: None,
3070                                inferred_type: None,
3071                            })));
3072                        }
3073                    }
3074                }
3075                Ok(Expression::Function(Box::new(f)))
3076            }
3077
3078            // UNIFORM -> keep as-is (Snowflake-specific)
3079            "UNIFORM" => Ok(Expression::Function(Box::new(f))),
3080
3081            // REPLACE with 2 args -> add empty string 3rd arg
3082            "REPLACE" if f.args.len() == 2 => {
3083                let mut args = f.args;
3084                args.push(Expression::Literal(Box::new(
3085                    crate::expressions::Literal::String(String::new()),
3086                )));
3087                Ok(Expression::Function(Box::new(Function::new(
3088                    "REPLACE".to_string(),
3089                    args,
3090                ))))
3091            }
3092
3093            // ARBITRARY -> ANY_VALUE in Snowflake
3094            "ARBITRARY" => Ok(Expression::Function(Box::new(Function::new(
3095                "ANY_VALUE".to_string(),
3096                f.args,
3097            )))),
3098
3099            // SAFE_DIVIDE(x, y) -> IFF(y <> 0, x / y, NULL)
3100            "SAFE_DIVIDE" if f.args.len() == 2 => {
3101                let mut args = f.args;
3102                let x = args.remove(0);
3103                let y = args.remove(0);
3104                Ok(Expression::IfFunc(Box::new(crate::expressions::IfFunc {
3105                    condition: Expression::Neq(Box::new(BinaryOp {
3106                        left: y.clone(),
3107                        right: Expression::number(0),
3108                        left_comments: Vec::new(),
3109                        operator_comments: Vec::new(),
3110                        trailing_comments: Vec::new(),
3111                        inferred_type: None,
3112                    })),
3113                    true_value: Expression::Div(Box::new(BinaryOp {
3114                        left: x,
3115                        right: y,
3116                        left_comments: Vec::new(),
3117                        operator_comments: Vec::new(),
3118                        trailing_comments: Vec::new(),
3119                        inferred_type: None,
3120                    })),
3121                    false_value: Some(Expression::Null(crate::expressions::Null)),
3122                    original_name: Some("IFF".to_string()),
3123                    inferred_type: None,
3124                })))
3125            }
3126
3127            // TIMESTAMP(x) -> CAST(x AS TIMESTAMPTZ) in Snowflake
3128            "TIMESTAMP" if f.args.len() == 1 => {
3129                let arg = f.args.into_iter().next().unwrap();
3130                Ok(Expression::Cast(Box::new(Cast {
3131                    this: arg,
3132                    to: DataType::Custom {
3133                        name: "TIMESTAMPTZ".to_string(),
3134                    },
3135                    trailing_comments: Vec::new(),
3136                    double_colon_syntax: false,
3137                    format: None,
3138                    default: None,
3139                    inferred_type: None,
3140                })))
3141            }
3142
3143            // TIMESTAMP(x, tz) -> CONVERT_TIMEZONE(tz, CAST(x AS TIMESTAMP)) in Snowflake
3144            "TIMESTAMP" if f.args.len() == 2 => {
3145                let mut args = f.args;
3146                let value = args.remove(0);
3147                let tz = args.remove(0);
3148                Ok(Expression::Function(Box::new(Function::new(
3149                    "CONVERT_TIMEZONE".to_string(),
3150                    vec![
3151                        tz,
3152                        Expression::Cast(Box::new(Cast {
3153                            this: value,
3154                            to: DataType::Timestamp {
3155                                precision: None,
3156                                timezone: false,
3157                            },
3158                            trailing_comments: Vec::new(),
3159                            double_colon_syntax: false,
3160                            format: None,
3161                            default: None,
3162                            inferred_type: None,
3163                        })),
3164                    ],
3165                ))))
3166            }
3167
3168            // TIME(h, m, s) -> TIME_FROM_PARTS(h, m, s) in Snowflake
3169            "TIME" if f.args.len() == 3 => Ok(Expression::Function(Box::new(Function::new(
3170                "TIME_FROM_PARTS".to_string(),
3171                f.args,
3172            )))),
3173
3174            // DIV0(x, y) -> IFF(y = 0 AND NOT x IS NULL, 0, x / y)
3175            "DIV0" if f.args.len() == 2 => {
3176                let mut args = f.args;
3177                let x = args.remove(0);
3178                let y = args.remove(0);
3179                // Need parens around complex expressions
3180                let x_expr = Self::maybe_paren(x.clone());
3181                let y_expr = Self::maybe_paren(y.clone());
3182                Ok(Expression::IfFunc(Box::new(crate::expressions::IfFunc {
3183                    condition: Expression::And(Box::new(BinaryOp::new(
3184                        Expression::Eq(Box::new(BinaryOp::new(
3185                            y_expr.clone(),
3186                            Expression::number(0),
3187                        ))),
3188                        Expression::Not(Box::new(crate::expressions::UnaryOp {
3189                            this: Expression::IsNull(Box::new(crate::expressions::IsNull {
3190                                this: x_expr.clone(),
3191                                not: false,
3192                                postfix_form: false,
3193                            })),
3194                            inferred_type: None,
3195                        })),
3196                    ))),
3197                    true_value: Expression::number(0),
3198                    false_value: Some(Expression::Div(Box::new(BinaryOp::new(x_expr, y_expr)))),
3199                    original_name: Some("IFF".to_string()),
3200                    inferred_type: None,
3201                })))
3202            }
3203
3204            // DIV0NULL(x, y) -> IFF(y = 0 OR y IS NULL, 0, x / y)
3205            "DIV0NULL" if f.args.len() == 2 => {
3206                let mut args = f.args;
3207                let x = args.remove(0);
3208                let y = args.remove(0);
3209                let x_expr = Self::maybe_paren(x.clone());
3210                let y_expr = Self::maybe_paren(y.clone());
3211                Ok(Expression::IfFunc(Box::new(crate::expressions::IfFunc {
3212                    condition: Expression::Or(Box::new(BinaryOp::new(
3213                        Expression::Eq(Box::new(BinaryOp::new(
3214                            y_expr.clone(),
3215                            Expression::number(0),
3216                        ))),
3217                        Expression::IsNull(Box::new(crate::expressions::IsNull {
3218                            this: y_expr.clone(),
3219                            not: false,
3220                            postfix_form: false,
3221                        })),
3222                    ))),
3223                    true_value: Expression::number(0),
3224                    false_value: Some(Expression::Div(Box::new(BinaryOp::new(x_expr, y_expr)))),
3225                    original_name: Some("IFF".to_string()),
3226                    inferred_type: None,
3227                })))
3228            }
3229
3230            // ZEROIFNULL(x) -> IFF(x IS NULL, 0, x)
3231            "ZEROIFNULL" if f.args.len() == 1 => {
3232                let x = f.args.into_iter().next().unwrap();
3233                Ok(Expression::IfFunc(Box::new(crate::expressions::IfFunc {
3234                    condition: Expression::IsNull(Box::new(crate::expressions::IsNull {
3235                        this: x.clone(),
3236                        not: false,
3237                        postfix_form: false,
3238                    })),
3239                    true_value: Expression::number(0),
3240                    false_value: Some(x),
3241                    original_name: Some("IFF".to_string()),
3242                    inferred_type: None,
3243                })))
3244            }
3245
3246            // NULLIFZERO(x) -> IFF(x = 0, NULL, x)
3247            "NULLIFZERO" if f.args.len() == 1 => {
3248                let x = f.args.into_iter().next().unwrap();
3249                Ok(Expression::IfFunc(Box::new(crate::expressions::IfFunc {
3250                    condition: Expression::Eq(Box::new(BinaryOp::new(
3251                        x.clone(),
3252                        Expression::number(0),
3253                    ))),
3254                    true_value: Expression::Null(crate::expressions::Null),
3255                    false_value: Some(x),
3256                    original_name: Some("IFF".to_string()),
3257                    inferred_type: None,
3258                })))
3259            }
3260
3261            // TRY_TO_TIME('string') -> TRY_CAST('string' AS TIME) when single string arg
3262            "TRY_TO_TIME" => {
3263                if f.args.len() == 1 {
3264                    if let Expression::Literal(lit) = &f.args[0] {
3265                        if let crate::expressions::Literal::String(_) = lit.as_ref() {
3266                            return Ok(Expression::TryCast(Box::new(Cast {
3267                                this: f.args.into_iter().next().unwrap(),
3268                                to: crate::expressions::DataType::Time {
3269                                    precision: None,
3270                                    timezone: false,
3271                                },
3272                                double_colon_syntax: false,
3273                                trailing_comments: Vec::new(),
3274                                format: None,
3275                                default: None,
3276                                inferred_type: None,
3277                            })));
3278                        }
3279                    }
3280                }
3281                // Normalize format string (2nd arg) if present
3282                let mut args = f.args;
3283                if args.len() >= 2 {
3284                    args[1] = Self::normalize_format_arg(args[1].clone());
3285                }
3286                Ok(Expression::Function(Box::new(Function::new(
3287                    "TRY_TO_TIME".to_string(),
3288                    args,
3289                ))))
3290            }
3291
3292            // TRY_TO_TIMESTAMP('string') -> TRY_CAST('string' AS TIMESTAMP) when single string arg
3293            // Convert if the string is NOT a pure numeric/epoch value
3294            "TRY_TO_TIMESTAMP" => {
3295                if f.args.len() == 1 {
3296                    if let Expression::Literal(lit) = &f.args[0] {
3297                        if let crate::expressions::Literal::String(s) = lit.as_ref() {
3298                            if !Self::looks_like_epoch(s) {
3299                                return Ok(Expression::TryCast(Box::new(Cast {
3300                                    this: f.args.into_iter().next().unwrap(),
3301                                    to: DataType::Timestamp {
3302                                        precision: None,
3303                                        timezone: false,
3304                                    },
3305                                    double_colon_syntax: false,
3306                                    trailing_comments: Vec::new(),
3307                                    format: None,
3308                                    default: None,
3309                                    inferred_type: None,
3310                                })));
3311                            }
3312                        }
3313                    }
3314                }
3315                // Normalize format string (2nd arg) if present
3316                let mut args = f.args;
3317                if args.len() >= 2 {
3318                    args[1] = Self::normalize_format_arg(args[1].clone());
3319                }
3320                Ok(Expression::Function(Box::new(Function::new(
3321                    "TRY_TO_TIMESTAMP".to_string(),
3322                    args,
3323                ))))
3324            }
3325
3326            // TRY_TO_DATE('string') -> TRY_CAST('string' AS DATE) when single string arg
3327            "TRY_TO_DATE" => {
3328                if f.args.len() == 1 {
3329                    if let Expression::Literal(lit) = &f.args[0] {
3330                        if let crate::expressions::Literal::String(s) = lit.as_ref() {
3331                            // Only convert if the string looks like a date
3332                            if s.contains('-') && s.len() >= 8 && s.len() <= 12 {
3333                                return Ok(Expression::TryCast(Box::new(Cast {
3334                                    this: f.args.into_iter().next().unwrap(),
3335                                    to: crate::expressions::DataType::Date,
3336                                    double_colon_syntax: false,
3337                                    trailing_comments: Vec::new(),
3338                                    format: None,
3339                                    default: None,
3340                                    inferred_type: None,
3341                                })));
3342                            }
3343                        }
3344                    }
3345                }
3346                // Normalize format string (2nd arg) if present
3347                let mut args = f.args;
3348                if args.len() >= 2 {
3349                    args[1] = Self::normalize_format_arg(args[1].clone());
3350                }
3351                Ok(Expression::Function(Box::new(Function::new(
3352                    "TRY_TO_DATE".to_string(),
3353                    args,
3354                ))))
3355            }
3356
3357            // TRY_TO_DOUBLE -> keep as TRY_TO_DOUBLE in Snowflake (native function)
3358            "TRY_TO_DOUBLE" => Ok(Expression::Function(Box::new(f))),
3359
3360            // REGEXP_REPLACE with 2 args -> add empty string replacement
3361            "REGEXP_REPLACE" if f.args.len() == 2 => {
3362                let mut args = f.args;
3363                args.push(Expression::Literal(Box::new(
3364                    crate::expressions::Literal::String(String::new()),
3365                )));
3366                Ok(Expression::Function(Box::new(Function::new(
3367                    "REGEXP_REPLACE".to_string(),
3368                    args,
3369                ))))
3370            }
3371
3372            // LAST_DAY(x, MONTH) -> LAST_DAY(x) in Snowflake (strip MONTH default)
3373            "LAST_DAY" if f.args.len() == 2 => {
3374                let mut args = f.args;
3375                let date = args.remove(0);
3376                let unit = args.remove(0);
3377                let unit_str = match &unit {
3378                    Expression::Column(c) => c.name.name.to_uppercase(),
3379                    Expression::Identifier(i) => i.name.to_uppercase(),
3380                    _ => String::new(),
3381                };
3382                if unit_str == "MONTH" {
3383                    Ok(Expression::Function(Box::new(Function::new(
3384                        "LAST_DAY".to_string(),
3385                        vec![date],
3386                    ))))
3387                } else {
3388                    Ok(Expression::Function(Box::new(Function::new(
3389                        "LAST_DAY".to_string(),
3390                        vec![date, unit],
3391                    ))))
3392                }
3393            }
3394
3395            // EXTRACT('field', expr) function-call syntax -> DATE_PART('field', expr)
3396            "EXTRACT" if f.args.len() == 2 => Ok(Expression::Function(Box::new(Function::new(
3397                "DATE_PART".to_string(),
3398                f.args,
3399            )))),
3400
3401            // ENDS_WITH/ENDSWITH -> EndsWith AST node
3402            "ENDS_WITH" | "ENDSWITH" if f.args.len() == 2 => {
3403                let mut args = f.args;
3404                let this = args.remove(0);
3405                let expr = args.remove(0);
3406                Ok(Expression::EndsWith(Box::new(
3407                    crate::expressions::BinaryFunc {
3408                        original_name: None,
3409                        this,
3410                        expression: expr,
3411                        inferred_type: None,
3412                    },
3413                )))
3414            }
3415
3416            // Pass through everything else
3417            _ => Ok(Expression::Function(Box::new(f))),
3418        }
3419    }
3420
3421    /// Check if a string looks like a datetime (contains date separators, not just digits)
3422    fn looks_like_datetime(s: &str) -> bool {
3423        // A datetime string typically contains dashes, colons, or spaces
3424        // A numeric/epoch string is just digits (possibly with a dot)
3425        s.contains('-') || s.contains(':') || s.contains(' ') || s.contains('/')
3426    }
3427
3428    /// Check if a string looks like an epoch number (only digits, possibly with a dot)
3429    fn looks_like_epoch(s: &str) -> bool {
3430        !s.is_empty() && s.chars().all(|c| c.is_ascii_digit() || c == '.')
3431    }
3432
3433    /// Wrap an expression in parentheses if it's a complex expression (binary op, etc.)
3434    fn maybe_paren(expr: Expression) -> Expression {
3435        match &expr {
3436            Expression::Sub(_) | Expression::Add(_) | Expression::Mul(_) | Expression::Div(_) => {
3437                Expression::Paren(Box::new(crate::expressions::Paren {
3438                    this: expr,
3439                    trailing_comments: Vec::new(),
3440                }))
3441            }
3442            _ => expr,
3443        }
3444    }
3445
3446    /// Normalize Snowflake date/time format strings to canonical lowercase form.
3447    /// YYYY -> yyyy, MM -> mm, DD -> DD (stays), HH24 -> hh24, HH12 -> hh12,
3448    /// MI -> mi, SS -> ss, FF -> ff, AM/PM -> pm, quoted "T" -> T
3449    fn normalize_snowflake_format(format: &str) -> String {
3450        let mut result = String::new();
3451        let chars: Vec<char> = format.chars().collect();
3452        let mut i = 0;
3453        while i < chars.len() {
3454            // Handle quoted strings like "T" -> T
3455            if chars[i] == '"' {
3456                i += 1;
3457                while i < chars.len() && chars[i] != '"' {
3458                    result.push(chars[i]);
3459                    i += 1;
3460                }
3461                if i < chars.len() {
3462                    i += 1; // skip closing quote
3463                }
3464                continue;
3465            }
3466
3467            let remaining = &format[i..];
3468            let remaining_upper = remaining.to_uppercase();
3469
3470            // Multi-char patterns (check longest first)
3471            if remaining_upper.starts_with("YYYY") {
3472                result.push_str("yyyy");
3473                i += 4;
3474            } else if remaining_upper.starts_with("YY") {
3475                result.push_str("yy");
3476                i += 2;
3477            } else if remaining_upper.starts_with("MMMM") {
3478                result.push_str("mmmm");
3479                i += 4;
3480            } else if remaining_upper.starts_with("MON") {
3481                result.push_str("mon");
3482                i += 3;
3483            } else if remaining_upper.starts_with("MM") {
3484                result.push_str("mm");
3485                i += 2;
3486            } else if remaining_upper.starts_with("DD") {
3487                result.push_str("DD");
3488                i += 2;
3489            } else if remaining_upper.starts_with("DY") {
3490                result.push_str("dy");
3491                i += 2;
3492            } else if remaining_upper.starts_with("HH24") {
3493                result.push_str("hh24");
3494                i += 4;
3495            } else if remaining_upper.starts_with("HH12") {
3496                result.push_str("hh12");
3497                i += 4;
3498            } else if remaining_upper.starts_with("HH") {
3499                result.push_str("hh");
3500                i += 2;
3501            } else if remaining_upper.starts_with("MISS") {
3502                // MISS = MI + SS
3503                result.push_str("miss");
3504                i += 4;
3505            } else if remaining_upper.starts_with("MI") {
3506                result.push_str("mi");
3507                i += 2;
3508            } else if remaining_upper.starts_with("SS") {
3509                result.push_str("ss");
3510                i += 2;
3511            } else if remaining_upper.starts_with("FF") {
3512                // FF followed by a digit (FF1-FF9) keeps the digit
3513                let ff_len = 2;
3514                let digit = if i + ff_len < chars.len() && chars[i + ff_len].is_ascii_digit() {
3515                    let d = chars[i + ff_len];
3516                    Some(d)
3517                } else {
3518                    None
3519                };
3520                if let Some(d) = digit {
3521                    result.push_str("ff");
3522                    result.push(d);
3523                    i += 3;
3524                } else {
3525                    // Plain FF -> ff9
3526                    result.push_str("ff9");
3527                    i += 2;
3528                }
3529            } else if remaining_upper.starts_with("AM") || remaining_upper.starts_with("PM") {
3530                result.push_str("pm");
3531                i += 2;
3532            } else if remaining_upper.starts_with("TZH") {
3533                result.push_str("tzh");
3534                i += 3;
3535            } else if remaining_upper.starts_with("TZM") {
3536                result.push_str("tzm");
3537                i += 3;
3538            } else {
3539                // Keep separators and other characters as-is
3540                result.push(chars[i]);
3541                i += 1;
3542            }
3543        }
3544        result
3545    }
3546
3547    /// Normalize format string argument if it's a string literal
3548    fn normalize_format_arg(expr: Expression) -> Expression {
3549        if let Expression::Literal(lit) = &expr {
3550            if let crate::expressions::Literal::String(s) = lit.as_ref() {
3551                let normalized = Self::normalize_snowflake_format(s);
3552                Expression::Literal(Box::new(crate::expressions::Literal::String(normalized)))
3553            } else {
3554                expr.clone()
3555            }
3556        } else {
3557            expr
3558        }
3559    }
3560
3561    /// Transform a lambda with typed parameters for Snowflake
3562    /// For `a int -> a + a + 1`, transforms to `a -> CAST(a AS INT) + CAST(a AS INT) + 1`
3563    fn transform_typed_lambda(&self, lambda: crate::expressions::LambdaExpr) -> Expression {
3564        use crate::expressions::{DataType, LambdaExpr};
3565        use std::collections::HashMap;
3566
3567        // Build mapping of parameter names to their types
3568        let mut param_types: HashMap<String, DataType> = HashMap::new();
3569        for (i, param) in lambda.parameters.iter().enumerate() {
3570            if let Some(Some(dt)) = lambda.parameter_types.get(i) {
3571                param_types.insert(param.name.to_uppercase(), dt.clone());
3572            }
3573        }
3574
3575        // If no typed parameters, return lambda unchanged
3576        if param_types.is_empty() {
3577            return Expression::Lambda(Box::new(lambda));
3578        }
3579
3580        // Transform the body by replacing parameter references with CAST expressions
3581        let transformed_body = self.replace_lambda_params_with_cast(lambda.body, &param_types);
3582
3583        // Return new lambda without type annotations (they're now embedded in CAST)
3584        Expression::Lambda(Box::new(LambdaExpr {
3585            parameters: lambda.parameters,
3586            body: transformed_body,
3587            colon: lambda.colon,
3588            parameter_types: Vec::new(), // Clear type annotations
3589        }))
3590    }
3591
3592    /// Recursively replace column/identifier references to typed lambda parameters with CAST expressions
3593    fn replace_lambda_params_with_cast(
3594        &self,
3595        expr: Expression,
3596        param_types: &std::collections::HashMap<String, crate::expressions::DataType>,
3597    ) -> Expression {
3598        use crate::expressions::{BinaryOp, Cast, Paren};
3599
3600        match expr {
3601            // Column reference - check if it matches a typed parameter
3602            Expression::Column(col) if col.table.is_none() => {
3603                let name_upper = col.name.name.to_uppercase();
3604                if let Some(dt) = param_types.get(&name_upper) {
3605                    // Wrap in CAST
3606                    Expression::Cast(Box::new(Cast {
3607                        this: Expression::Column(col),
3608                        to: dt.clone(),
3609                        double_colon_syntax: false,
3610                        trailing_comments: Vec::new(),
3611                        format: None,
3612                        default: None,
3613                        inferred_type: None,
3614                    }))
3615                } else {
3616                    Expression::Column(col)
3617                }
3618            }
3619
3620            // Identifier reference - check if it matches a typed parameter
3621            Expression::Identifier(id) => {
3622                let name_upper = id.name.to_uppercase();
3623                if let Some(dt) = param_types.get(&name_upper) {
3624                    // Wrap in CAST
3625                    Expression::Cast(Box::new(Cast {
3626                        this: Expression::Identifier(id),
3627                        to: dt.clone(),
3628                        double_colon_syntax: false,
3629                        trailing_comments: Vec::new(),
3630                        format: None,
3631                        default: None,
3632                        inferred_type: None,
3633                    }))
3634                } else {
3635                    Expression::Identifier(id)
3636                }
3637            }
3638
3639            // Binary operations - recursively transform both sides
3640            Expression::Add(op) => Expression::Add(Box::new(BinaryOp::new(
3641                self.replace_lambda_params_with_cast(op.left, param_types),
3642                self.replace_lambda_params_with_cast(op.right, param_types),
3643            ))),
3644            Expression::Sub(op) => Expression::Sub(Box::new(BinaryOp::new(
3645                self.replace_lambda_params_with_cast(op.left, param_types),
3646                self.replace_lambda_params_with_cast(op.right, param_types),
3647            ))),
3648            Expression::Mul(op) => Expression::Mul(Box::new(BinaryOp::new(
3649                self.replace_lambda_params_with_cast(op.left, param_types),
3650                self.replace_lambda_params_with_cast(op.right, param_types),
3651            ))),
3652            Expression::Div(op) => Expression::Div(Box::new(BinaryOp::new(
3653                self.replace_lambda_params_with_cast(op.left, param_types),
3654                self.replace_lambda_params_with_cast(op.right, param_types),
3655            ))),
3656            Expression::Mod(op) => Expression::Mod(Box::new(BinaryOp::new(
3657                self.replace_lambda_params_with_cast(op.left, param_types),
3658                self.replace_lambda_params_with_cast(op.right, param_types),
3659            ))),
3660
3661            // Parenthesized expression
3662            Expression::Paren(p) => Expression::Paren(Box::new(Paren {
3663                this: self.replace_lambda_params_with_cast(p.this, param_types),
3664                trailing_comments: p.trailing_comments,
3665            })),
3666
3667            // Function calls - transform arguments
3668            Expression::Function(mut f) => {
3669                f.args = f
3670                    .args
3671                    .into_iter()
3672                    .map(|arg| self.replace_lambda_params_with_cast(arg, param_types))
3673                    .collect();
3674                Expression::Function(f)
3675            }
3676
3677            // Comparison operators
3678            Expression::Eq(op) => Expression::Eq(Box::new(BinaryOp::new(
3679                self.replace_lambda_params_with_cast(op.left, param_types),
3680                self.replace_lambda_params_with_cast(op.right, param_types),
3681            ))),
3682            Expression::Neq(op) => Expression::Neq(Box::new(BinaryOp::new(
3683                self.replace_lambda_params_with_cast(op.left, param_types),
3684                self.replace_lambda_params_with_cast(op.right, param_types),
3685            ))),
3686            Expression::Lt(op) => Expression::Lt(Box::new(BinaryOp::new(
3687                self.replace_lambda_params_with_cast(op.left, param_types),
3688                self.replace_lambda_params_with_cast(op.right, param_types),
3689            ))),
3690            Expression::Lte(op) => Expression::Lte(Box::new(BinaryOp::new(
3691                self.replace_lambda_params_with_cast(op.left, param_types),
3692                self.replace_lambda_params_with_cast(op.right, param_types),
3693            ))),
3694            Expression::Gt(op) => Expression::Gt(Box::new(BinaryOp::new(
3695                self.replace_lambda_params_with_cast(op.left, param_types),
3696                self.replace_lambda_params_with_cast(op.right, param_types),
3697            ))),
3698            Expression::Gte(op) => Expression::Gte(Box::new(BinaryOp::new(
3699                self.replace_lambda_params_with_cast(op.left, param_types),
3700                self.replace_lambda_params_with_cast(op.right, param_types),
3701            ))),
3702
3703            // And/Or
3704            Expression::And(op) => Expression::And(Box::new(BinaryOp::new(
3705                self.replace_lambda_params_with_cast(op.left, param_types),
3706                self.replace_lambda_params_with_cast(op.right, param_types),
3707            ))),
3708            Expression::Or(op) => Expression::Or(Box::new(BinaryOp::new(
3709                self.replace_lambda_params_with_cast(op.left, param_types),
3710                self.replace_lambda_params_with_cast(op.right, param_types),
3711            ))),
3712
3713            // Other expressions - return unchanged
3714            other => other,
3715        }
3716    }
3717
3718    fn transform_aggregate_function(
3719        &self,
3720        f: Box<crate::expressions::AggregateFunction>,
3721    ) -> Result<Expression> {
3722        let name_upper = f.name.to_uppercase();
3723        match name_upper.as_str() {
3724            // GROUP_CONCAT -> LISTAGG
3725            "GROUP_CONCAT" if !f.args.is_empty() => Ok(Expression::Function(Box::new(
3726                Function::new("LISTAGG".to_string(), f.args),
3727            ))),
3728
3729            // STRING_AGG -> LISTAGG
3730            "STRING_AGG" if !f.args.is_empty() => Ok(Expression::Function(Box::new(
3731                Function::new("LISTAGG".to_string(), f.args),
3732            ))),
3733
3734            // APPROX_DISTINCT -> APPROX_COUNT_DISTINCT
3735            "APPROX_DISTINCT" if !f.args.is_empty() => Ok(Expression::Function(Box::new(
3736                Function::new("APPROX_COUNT_DISTINCT".to_string(), f.args),
3737            ))),
3738
3739            // BIT_AND -> BITAND_AGG
3740            "BIT_AND" if !f.args.is_empty() => Ok(Expression::Function(Box::new(Function::new(
3741                "BITAND_AGG".to_string(),
3742                f.args,
3743            )))),
3744
3745            // BIT_OR -> BITOR_AGG
3746            "BIT_OR" if !f.args.is_empty() => Ok(Expression::Function(Box::new(Function::new(
3747                "BITOR_AGG".to_string(),
3748                f.args,
3749            )))),
3750
3751            // BIT_XOR -> BITXOR_AGG
3752            "BIT_XOR" if !f.args.is_empty() => Ok(Expression::Function(Box::new(Function::new(
3753                "BITXOR_AGG".to_string(),
3754                f.args,
3755            )))),
3756
3757            // BOOL_AND/BOOLAND_AGG/LOGICAL_AND -> LogicalAnd AST node
3758            "BOOL_AND" | "LOGICAL_AND" | "BOOLAND_AGG" if !f.args.is_empty() => {
3759                let arg = f.args.into_iter().next().unwrap();
3760                Ok(Expression::LogicalAnd(Box::new(AggFunc {
3761                    this: arg,
3762                    distinct: f.distinct,
3763                    filter: f.filter,
3764                    order_by: Vec::new(),
3765                    name: Some("BOOLAND_AGG".to_string()),
3766                    ignore_nulls: None,
3767                    having_max: None,
3768                    limit: None,
3769                    inferred_type: None,
3770                })))
3771            }
3772
3773            // BOOL_OR/BOOLOR_AGG/LOGICAL_OR -> LogicalOr AST node
3774            "BOOL_OR" | "LOGICAL_OR" | "BOOLOR_AGG" if !f.args.is_empty() => {
3775                let arg = f.args.into_iter().next().unwrap();
3776                Ok(Expression::LogicalOr(Box::new(AggFunc {
3777                    this: arg,
3778                    distinct: f.distinct,
3779                    filter: f.filter,
3780                    order_by: Vec::new(),
3781                    name: Some("BOOLOR_AGG".to_string()),
3782                    ignore_nulls: None,
3783                    having_max: None,
3784                    limit: None,
3785                    inferred_type: None,
3786                })))
3787            }
3788
3789            // APPROX_TOP_K - add default k=1 if only one argument
3790            "APPROX_TOP_K" if f.args.len() == 1 => {
3791                let mut args = f.args;
3792                args.push(Expression::number(1));
3793                Ok(Expression::AggregateFunction(Box::new(
3794                    crate::expressions::AggregateFunction {
3795                        name: "APPROX_TOP_K".to_string(),
3796                        args,
3797                        distinct: f.distinct,
3798                        filter: f.filter,
3799                        order_by: Vec::new(),
3800                        limit: None,
3801                        ignore_nulls: None,
3802                        inferred_type: None,
3803                    },
3804                )))
3805            }
3806
3807            // SKEW/SKEWNESS -> Skewness AST node
3808            "SKEW" | "SKEWNESS" if !f.args.is_empty() => {
3809                let arg = f.args.into_iter().next().unwrap();
3810                Ok(Expression::Skewness(Box::new(AggFunc {
3811                    this: arg,
3812                    distinct: f.distinct,
3813                    filter: f.filter,
3814                    order_by: Vec::new(),
3815                    name: Some("SKEW".to_string()),
3816                    ignore_nulls: None,
3817                    having_max: None,
3818                    limit: None,
3819                    inferred_type: None,
3820                })))
3821            }
3822
3823            // Pass through everything else
3824            _ => Ok(Expression::AggregateFunction(f)),
3825        }
3826    }
3827}
3828
3829/// Convert strftime format specifiers to Snowflake format specifiers
3830fn strftime_to_snowflake_format(fmt: &str) -> String {
3831    let mut result = String::new();
3832    let chars: Vec<char> = fmt.chars().collect();
3833    let mut i = 0;
3834    while i < chars.len() {
3835        if chars[i] == '%' && i + 1 < chars.len() {
3836            match chars[i + 1] {
3837                'Y' => {
3838                    result.push_str("yyyy");
3839                    i += 2;
3840                }
3841                'y' => {
3842                    result.push_str("yy");
3843                    i += 2;
3844                }
3845                'm' => {
3846                    result.push_str("mm");
3847                    i += 2;
3848                }
3849                'd' => {
3850                    result.push_str("DD");
3851                    i += 2;
3852                }
3853                'H' => {
3854                    result.push_str("hh24");
3855                    i += 2;
3856                }
3857                'M' => {
3858                    result.push_str("mmmm");
3859                    i += 2;
3860                } // %M = full month name
3861                'i' => {
3862                    result.push_str("mi");
3863                    i += 2;
3864                }
3865                'S' | 's' => {
3866                    result.push_str("ss");
3867                    i += 2;
3868                }
3869                'f' => {
3870                    result.push_str("ff");
3871                    i += 2;
3872                }
3873                'w' => {
3874                    result.push_str("dy");
3875                    i += 2;
3876                } // day of week number
3877                'a' => {
3878                    result.push_str("DY");
3879                    i += 2;
3880                } // abbreviated day name
3881                'b' => {
3882                    result.push_str("mon");
3883                    i += 2;
3884                } // abbreviated month name
3885                'T' => {
3886                    result.push_str("hh24:mi:ss");
3887                    i += 2;
3888                } // time shorthand
3889                _ => {
3890                    result.push(chars[i]);
3891                    result.push(chars[i + 1]);
3892                    i += 2;
3893                }
3894            }
3895        } else {
3896            result.push(chars[i]);
3897            i += 1;
3898        }
3899    }
3900    result
3901}
3902
3903#[cfg(test)]
3904mod tests {
3905    use super::*;
3906    use crate::dialects::Dialect;
3907
3908    fn transpile_to_snowflake(sql: &str) -> String {
3909        let dialect = Dialect::get(DialectType::Generic);
3910        let result = dialect
3911            .transpile(sql, DialectType::Snowflake)
3912            .expect("Transpile failed");
3913        result[0].clone()
3914    }
3915
3916    #[test]
3917    fn test_ifnull_to_coalesce() {
3918        let result = transpile_to_snowflake("SELECT IFNULL(a, b)");
3919        assert!(
3920            result.contains("COALESCE"),
3921            "Expected COALESCE, got: {}",
3922            result
3923        );
3924    }
3925
3926    #[test]
3927    fn test_basic_select() {
3928        let result = transpile_to_snowflake("SELECT a, b FROM users WHERE id = 1");
3929        assert!(result.contains("SELECT"));
3930        assert!(result.contains("FROM users"));
3931    }
3932
3933    #[test]
3934    fn test_snowflake_scripting_cursor_declare_block_roundtrip() {
3935        let sql = "DECLARE
3936  emp CURSOR FOR SELECT salary FROM employees;
3937BEGIN
3938  RETURN 1;
3939END";
3940
3941        let dialect = Dialect::get(DialectType::Snowflake);
3942        let ast = dialect.parse(sql).expect("Parse failed");
3943        let output = dialect.generate(&ast[0]).expect("Generate failed");
3944
3945        assert_eq!(output, sql);
3946    }
3947
3948    #[test]
3949    fn test_snowflake_scripting_cursor_return_table_roundtrip() {
3950        let sql = "DECLARE
3951  c1 CURSOR FOR SELECT * FROM invoices;
3952BEGIN
3953  OPEN c1;
3954  RETURN TABLE(RESULTSET_FROM_CURSOR(c1));
3955END";
3956
3957        let dialect = Dialect::get(DialectType::Snowflake);
3958        let ast = dialect.parse(sql).expect("Parse failed");
3959        let output = dialect.generate(&ast[0]).expect("Generate failed");
3960
3961        assert_eq!(output, sql);
3962    }
3963
3964    #[test]
3965    fn test_group_concat_to_listagg() {
3966        let result = transpile_to_snowflake("SELECT GROUP_CONCAT(name)");
3967        assert!(
3968            result.contains("LISTAGG"),
3969            "Expected LISTAGG, got: {}",
3970            result
3971        );
3972    }
3973
3974    #[test]
3975    fn test_string_agg_to_listagg() {
3976        let result = transpile_to_snowflake("SELECT STRING_AGG(name)");
3977        assert!(
3978            result.contains("LISTAGG"),
3979            "Expected LISTAGG, got: {}",
3980            result
3981        );
3982    }
3983
3984    #[test]
3985    fn test_array_to_array_construct() {
3986        let result = transpile_to_snowflake("SELECT ARRAY(1, 2, 3)");
3987        // ARRAY(1, 2, 3) from Generic -> Snowflake uses [] bracket notation
3988        assert!(
3989            result.contains("[1, 2, 3]"),
3990            "Expected [1, 2, 3], got: {}",
3991            result
3992        );
3993    }
3994
3995    #[test]
3996    fn test_double_quote_identifiers() {
3997        // Snowflake uses double quotes for identifiers
3998        let dialect = SnowflakeDialect;
3999        let config = dialect.generator_config();
4000        assert_eq!(config.identifier_quote, '"');
4001    }
4002}