Skip to main content

polyglot_sql/dialects/
bigquery.rs

1//! BigQuery Dialect
2//!
3//! BigQuery-specific transformations based on sqlglot patterns.
4//! Key differences:
5//! - Uses backticks for identifiers (especially for project.dataset.table)
6//! - SAFE_ prefix for safe operations
7//! - Different date/time function names (DATE_DIFF, FORMAT_DATE, PARSE_DATE)
8//! - STRUCT and ARRAY syntax differences
9//! - No ILIKE support
10//! - QUALIFY clause support
11
12use super::{DialectImpl, DialectType};
13use crate::error::Result;
14use crate::expressions::{
15    Alias, BinaryOp, CeilFunc, Column, Exists, Expression, From, Function, FunctionBody,
16    Identifier, JsonExtractFunc, LikeOp, Literal, Select, SplitFunc, StringAggFunc, UnaryFunc,
17    UnnestFunc, VarArgFunc, Where,
18};
19#[cfg(feature = "generate")]
20use crate::generator::GeneratorConfig;
21use crate::tokens::TokenizerConfig;
22
23/// BigQuery dialect
24pub struct BigQueryDialect;
25
26impl DialectImpl for BigQueryDialect {
27    fn dialect_type(&self) -> DialectType {
28        DialectType::BigQuery
29    }
30
31    fn tokenizer_config(&self) -> TokenizerConfig {
32        let mut config = TokenizerConfig::default();
33        // BigQuery uses backticks for identifiers, NOT double quotes
34        // Remove double quote from identifiers (it's in the default config)
35        config.identifiers.remove(&'"');
36        config.identifiers.insert('`', '`');
37        config.identifier_backslash_escapes = true;
38        // BigQuery supports double quotes for strings (in addition to single quotes)
39        config.quotes.insert("\"".to_string(), "\"".to_string());
40        // BigQuery supports triple-quoted strings
41        config.quotes.insert("'''".to_string(), "'''".to_string());
42        config
43            .quotes
44            .insert("\"\"\"".to_string(), "\"\"\"".to_string());
45        // BigQuery supports backslash escaping in strings
46        config.string_escapes = vec!['\'', '\\'];
47        // In BigQuery, b'...' is a byte string (bytes), not a bit string (binary digits)
48        config.b_prefix_is_byte_string = true;
49        // BigQuery supports hex number strings like 0xA, 0xFF
50        config.hex_number_strings = true;
51        // BigQuery: 0xA represents integer 10 (not binary/blob)
52        config.hex_string_is_integer_type = true;
53        // BigQuery supports # as single-line comments.
54        config.hash_comments = true;
55        config
56    }
57
58    #[cfg(feature = "generate")]
59
60    fn generator_config(&self) -> GeneratorConfig {
61        use crate::generator::{IdentifierQuoteStyle, NormalizeFunctions};
62        GeneratorConfig {
63            identifier_quote: '`',
64            identifier_quote_style: IdentifierQuoteStyle::BACKTICK,
65            dialect: Some(DialectType::BigQuery),
66            // BigQuery doesn't normalize function names (Python: NORMALIZE_FUNCTIONS = False)
67            normalize_functions: NormalizeFunctions::None,
68            // BigQuery-specific settings from Python sqlglot
69            interval_allows_plural_form: false,
70            join_hints: false,
71            query_hints: false,
72            table_hints: false,
73            limit_fetch_style: crate::generator::LimitFetchStyle::Limit,
74            rename_table_with_db: false,
75            nvl2_supported: false,
76            unnest_with_ordinality: false,
77            collate_is_func: true,
78            limit_only_literals: true,
79            supports_table_alias_columns: false,
80            unpivot_aliases_are_identifiers: false,
81            json_key_value_pair_sep: ",",
82            null_ordering_supported: false,
83            ignore_nulls_in_func: true,
84            json_path_single_quote_escape: true,
85            can_implement_array_any: true,
86            supports_to_number: false,
87            named_placeholder_token: "@",
88            hex_func: "TO_HEX",
89            with_properties_prefix: "OPTIONS",
90            supports_exploding_projections: false,
91            except_intersect_support_all_clause: false,
92            supports_unix_seconds: true,
93            // BigQuery uses SAFE_ prefix for safe operations
94            try_supported: true,
95            // BigQuery does not support SEMI/ANTI JOIN syntax
96            semi_anti_join_with_side: false,
97            ..Default::default()
98        }
99    }
100
101    #[cfg(feature = "transpile")]
102
103    fn transform_expr(&self, expr: Expression) -> Result<Expression> {
104        match expr {
105            // ===== Data Type Mappings =====
106            Expression::DataType(dt) => self.transform_data_type(dt),
107
108            Expression::Table(mut table)
109                if table.catalog.is_none()
110                    && table.alias.is_none()
111                    && table.schema.as_ref().map_or(false, |schema| {
112                        schema.quoted && schema.name.contains("INFORMATION_SCHEMA")
113                    }) =>
114            {
115                let schema = table.schema.take().expect("schema checked above");
116                let old_name = table.name.clone();
117                let alias = old_name.name.clone();
118                table.name = Identifier {
119                    name: format!("{}.{}", schema.name, old_name.name),
120                    quoted: true,
121                    trailing_comments: old_name.trailing_comments,
122                    span: old_name.span,
123                };
124                table.alias = Some(Identifier::new(alias));
125                Ok(Expression::Table(table))
126            }
127
128            // ===== Null handling =====
129            // IFNULL is native to BigQuery - keep as-is for identity
130            Expression::IfNull(f) => Ok(Expression::IfNull(f)),
131
132            // NVL -> IFNULL in BigQuery (BigQuery uses IFNULL, not NVL)
133            Expression::Nvl(f) => Ok(Expression::IfNull(f)),
134
135            // Coalesce stays as Coalesce
136            Expression::Coalesce(f) => Ok(Expression::Coalesce(f)),
137
138            // ===== String aggregation =====
139            // GROUP_CONCAT -> STRING_AGG in BigQuery
140            Expression::GroupConcat(f) => Ok(Expression::StringAgg(Box::new(StringAggFunc {
141                this: f.this,
142                separator: f.separator,
143                order_by: f.order_by,
144                distinct: f.distinct,
145                filter: f.filter,
146                limit: None,
147                inferred_type: None,
148            }))),
149
150            // ===== Cast operations =====
151            // Cast data types are transformed by transform_recursive in mod.rs
152            // which calls transform_data_type via transform_expr(Expression::DataType(...))
153
154            // TryCast -> SafeCast in BigQuery with type transformation
155            Expression::TryCast(c) => {
156                let transformed_type = match self.transform_data_type(c.to)? {
157                    Expression::DataType(dt) => dt,
158                    _ => return Err(crate::error::Error::parse("Expected DataType", 0, 0, 0, 0)),
159                };
160                Ok(Expression::SafeCast(Box::new(crate::expressions::Cast {
161                    this: c.this,
162                    to: transformed_type,
163                    trailing_comments: c.trailing_comments,
164                    double_colon_syntax: c.double_colon_syntax,
165                    format: c.format,
166                    default: c.default,
167                    inferred_type: None,
168                })))
169            }
170
171            // ===== Pattern matching =====
172            // ILIKE -> LOWER() LIKE LOWER() in BigQuery (no ILIKE support)
173            Expression::ILike(op) => {
174                let lower_left = Expression::Lower(Box::new(UnaryFunc::new(op.left)));
175                let lower_right = Expression::Lower(Box::new(UnaryFunc::new(op.right)));
176                Ok(Expression::Like(Box::new(LikeOp {
177                    left: lower_left,
178                    right: lower_right,
179                    escape: op.escape,
180                    quantifier: op.quantifier,
181                    inferred_type: None,
182                })))
183            }
184
185            // RegexpLike -> REGEXP_CONTAINS in BigQuery
186            Expression::RegexpLike(f) => Ok(Expression::Function(Box::new(Function::new(
187                "REGEXP_CONTAINS".to_string(),
188                vec![f.this, f.pattern],
189            )))),
190
191            // ===== Array operations =====
192            // EXPLODE -> UNNEST in BigQuery
193            Expression::Explode(f) => Ok(Expression::Unnest(Box::new(
194                crate::expressions::UnnestFunc {
195                    this: f.this,
196                    expressions: Vec::new(),
197                    with_ordinality: false,
198                    alias: None,
199                    offset_alias: None,
200                    inferred_type: None,
201                },
202            ))),
203
204            // ExplodeOuter -> UNNEST with LEFT JOIN semantics
205            Expression::ExplodeOuter(f) => Ok(Expression::Unnest(Box::new(
206                crate::expressions::UnnestFunc {
207                    this: f.this,
208                    expressions: Vec::new(),
209                    with_ordinality: false,
210                    alias: None,
211                    offset_alias: None,
212                    inferred_type: None,
213                },
214            ))),
215
216            // GenerateSeries -> GENERATE_ARRAY in BigQuery
217            Expression::GenerateSeries(f) => {
218                let mut args = Vec::new();
219                if let Some(start) = f.start {
220                    args.push(*start);
221                }
222                if let Some(end) = f.end {
223                    args.push(*end);
224                }
225                if let Some(step) = f.step {
226                    args.push(*step);
227                }
228                Ok(Expression::Function(Box::new(Function::new(
229                    "GENERATE_ARRAY".to_string(),
230                    args,
231                ))))
232            }
233
234            // ===== Bitwise operations =====
235            // BitwiseAndAgg -> BIT_AND
236            Expression::BitwiseAndAgg(f) => Ok(Expression::Function(Box::new(Function::new(
237                "BIT_AND".to_string(),
238                vec![f.this],
239            )))),
240
241            // BitwiseOrAgg -> BIT_OR
242            Expression::BitwiseOrAgg(f) => Ok(Expression::Function(Box::new(Function::new(
243                "BIT_OR".to_string(),
244                vec![f.this],
245            )))),
246
247            // BitwiseXorAgg -> BIT_XOR
248            Expression::BitwiseXorAgg(f) => Ok(Expression::Function(Box::new(Function::new(
249                "BIT_XOR".to_string(),
250                vec![f.this],
251            )))),
252
253            // BitwiseCount -> BIT_COUNT
254            Expression::BitwiseCount(f) => Ok(Expression::Function(Box::new(Function::new(
255                "BIT_COUNT".to_string(),
256                vec![f.this],
257            )))),
258
259            // ByteLength -> BYTE_LENGTH
260            Expression::ByteLength(f) => Ok(Expression::Function(Box::new(Function::new(
261                "BYTE_LENGTH".to_string(),
262                vec![f.this],
263            )))),
264
265            // IntDiv -> DIV
266            Expression::IntDiv(f) => Ok(Expression::Function(Box::new(Function::new(
267                "DIV".to_string(),
268                vec![f.this, f.expression],
269            )))),
270
271            // Int64 -> INT64
272            Expression::Int64(f) => Ok(Expression::Function(Box::new(Function::new(
273                "INT64".to_string(),
274                vec![f.this],
275            )))),
276
277            // ===== Random =====
278            // RANDOM -> RAND in BigQuery
279            Expression::Random(_) => Ok(Expression::Rand(Box::new(crate::expressions::Rand {
280                seed: None,
281                lower: None,
282                upper: None,
283            }))),
284
285            // ===== UUID =====
286            // Uuid -> GENERATE_UUID in BigQuery
287            Expression::Uuid(_) => Ok(Expression::Function(Box::new(Function::new(
288                "GENERATE_UUID".to_string(),
289                vec![],
290            )))),
291
292            // ===== Approximate functions =====
293            // ApproxDistinct -> APPROX_COUNT_DISTINCT
294            Expression::ApproxDistinct(f) => Ok(Expression::Function(Box::new(Function::new(
295                "APPROX_COUNT_DISTINCT".to_string(),
296                vec![f.this],
297            )))),
298
299            // ArgMax -> MAX_BY in BigQuery
300            Expression::ArgMax(f) => Ok(Expression::Function(Box::new(Function::new(
301                "MAX_BY".to_string(),
302                vec![*f.this, *f.expression],
303            )))),
304
305            // ArgMin -> MIN_BY in BigQuery
306            Expression::ArgMin(f) => Ok(Expression::Function(Box::new(Function::new(
307                "MIN_BY".to_string(),
308                vec![*f.this, *f.expression],
309            )))),
310
311            // ===== Conditional =====
312            // CountIf -> COUNTIF in BigQuery
313            Expression::CountIf(f) => Ok(Expression::Function(Box::new(Function::new(
314                "COUNTIF".to_string(),
315                vec![f.this],
316            )))),
317
318            // ===== String functions =====
319            // StringAgg -> STRING_AGG in BigQuery - keep as-is to preserve ORDER BY
320            Expression::StringAgg(f) => Ok(Expression::StringAgg(f)),
321
322            // ===== Conversion =====
323            // Unhex -> FROM_HEX
324            Expression::Unhex(f) => Ok(Expression::Function(Box::new(Function::new(
325                "FROM_HEX".to_string(),
326                vec![*f.this],
327            )))),
328
329            // UnixToTime -> TIMESTAMP_SECONDS/MILLIS/MICROS based on scale
330            Expression::UnixToTime(f) => {
331                let scale = f.scale.unwrap_or(0);
332                match scale {
333                    0 => Ok(Expression::Function(Box::new(Function::new(
334                        "TIMESTAMP_SECONDS".to_string(),
335                        vec![*f.this],
336                    )))),
337                    3 => Ok(Expression::Function(Box::new(Function::new(
338                        "TIMESTAMP_MILLIS".to_string(),
339                        vec![*f.this],
340                    )))),
341                    6 => Ok(Expression::Function(Box::new(Function::new(
342                        "TIMESTAMP_MICROS".to_string(),
343                        vec![*f.this],
344                    )))),
345                    _ => {
346                        // TIMESTAMP_SECONDS(CAST(value / POWER(10, scale) AS INT64))
347                        let div_expr =
348                            Expression::Div(Box::new(crate::expressions::BinaryOp::new(
349                                *f.this,
350                                Expression::Function(Box::new(Function::new(
351                                    "POWER".to_string(),
352                                    vec![Expression::number(10), Expression::number(scale)],
353                                ))),
354                            )));
355                        let cast_expr = Expression::Cast(Box::new(crate::expressions::Cast {
356                            this: div_expr,
357                            to: crate::expressions::DataType::Custom {
358                                name: "INT64".to_string(),
359                            },
360                            double_colon_syntax: false,
361                            trailing_comments: vec![],
362                            format: None,
363                            default: None,
364                            inferred_type: None,
365                        }));
366                        Ok(Expression::Function(Box::new(Function::new(
367                            "TIMESTAMP_SECONDS".to_string(),
368                            vec![cast_expr],
369                        ))))
370                    }
371                }
372            }
373
374            // ===== Date/time =====
375            // DateDiff -> DATE_DIFF in BigQuery
376            Expression::DateDiff(f) => {
377                // BigQuery: DATE_DIFF(date1, date2, part)
378                let unit_str = match f.unit {
379                    Some(crate::expressions::IntervalUnit::Year) => "YEAR",
380                    Some(crate::expressions::IntervalUnit::Quarter) => "QUARTER",
381                    Some(crate::expressions::IntervalUnit::Month) => "MONTH",
382                    Some(crate::expressions::IntervalUnit::Week) => "WEEK",
383                    Some(crate::expressions::IntervalUnit::Day) => "DAY",
384                    Some(crate::expressions::IntervalUnit::Hour) => "HOUR",
385                    Some(crate::expressions::IntervalUnit::Minute) => "MINUTE",
386                    Some(crate::expressions::IntervalUnit::Second) => "SECOND",
387                    Some(crate::expressions::IntervalUnit::Millisecond) => "MILLISECOND",
388                    Some(crate::expressions::IntervalUnit::Microsecond) => "MICROSECOND",
389                    Some(crate::expressions::IntervalUnit::Nanosecond) => "NANOSECOND",
390                    None => "DAY",
391                };
392                let unit = Expression::Identifier(crate::expressions::Identifier {
393                    name: unit_str.to_string(),
394                    quoted: false,
395                    trailing_comments: Vec::new(),
396                    span: None,
397                });
398                Ok(Expression::Function(Box::new(Function::new(
399                    "DATE_DIFF".to_string(),
400                    vec![f.this, f.expression, unit],
401                ))))
402            }
403
404            // ===== Variance =====
405            // VarPop -> VAR_POP
406            Expression::VarPop(f) => Ok(Expression::Function(Box::new(Function::new(
407                "VAR_POP".to_string(),
408                vec![f.this],
409            )))),
410
411            // ===== Hash functions =====
412            // SHA -> SHA1
413            Expression::SHA(f) => Ok(Expression::Function(Box::new(Function::new(
414                "SHA1".to_string(),
415                vec![f.this],
416            )))),
417
418            // SHA1Digest -> SHA1
419            Expression::SHA1Digest(f) => Ok(Expression::Function(Box::new(Function::new(
420                "SHA1".to_string(),
421                vec![f.this],
422            )))),
423
424            // MD5Digest -> MD5
425            Expression::MD5Digest(f) => Ok(Expression::Function(Box::new(Function::new(
426                "MD5".to_string(),
427                vec![*f.this],
428            )))),
429
430            // ===== Type conversion =====
431            // JSONBool -> BOOL
432            Expression::JSONBool(f) => Ok(Expression::Function(Box::new(Function::new(
433                "BOOL".to_string(),
434                vec![f.this],
435            )))),
436
437            // StringFunc -> STRING
438            Expression::StringFunc(f) => Ok(Expression::Function(Box::new(Function::new(
439                "STRING".to_string(),
440                vec![*f.this],
441            )))),
442
443            // ===== Date/time from parts =====
444            // DateFromUnixDate -> DATE_FROM_UNIX_DATE
445            Expression::DateFromUnixDate(f) => Ok(Expression::Function(Box::new(Function::new(
446                "DATE_FROM_UNIX_DATE".to_string(),
447                vec![f.this],
448            )))),
449
450            // UnixDate -> UNIX_DATE
451            Expression::UnixDate(f) => Ok(Expression::Function(Box::new(Function::new(
452                "UNIX_DATE".to_string(),
453                vec![f.this],
454            )))),
455
456            // TimestampDiff -> TIMESTAMP_DIFF
457            Expression::TimestampDiff(f) => Ok(Expression::Function(Box::new(Function::new(
458                "TIMESTAMP_DIFF".to_string(),
459                vec![*f.this, *f.expression],
460            )))),
461
462            // FromTimeZone -> DATETIME
463            Expression::FromTimeZone(f) => Ok(Expression::Function(Box::new(Function::new(
464                "DATETIME".to_string(),
465                vec![*f.this],
466            )))),
467
468            // TsOrDsToDatetime -> DATETIME
469            Expression::TsOrDsToDatetime(f) => Ok(Expression::Function(Box::new(Function::new(
470                "DATETIME".to_string(),
471                vec![f.this],
472            )))),
473
474            // TsOrDsToTimestamp -> TIMESTAMP
475            Expression::TsOrDsToTimestamp(f) => Ok(Expression::Function(Box::new(Function::new(
476                "TIMESTAMP".to_string(),
477                vec![f.this],
478            )))),
479
480            // Preserve typed BigQuery constructors through parsing, then expose their
481            // canonical spellings to cross-dialect normalization.
482            Expression::Timestamp(f) => {
483                let mut args = Vec::new();
484                if let Some(this) = f.this {
485                    args.push(*this);
486                }
487                if let Some(zone) = f.zone {
488                    args.push(*zone);
489                }
490                Ok(Expression::Function(Box::new(Function::new(
491                    "TIMESTAMP".to_string(),
492                    args,
493                ))))
494            }
495
496            Expression::Round(f) => {
497                let mut args = vec![f.this];
498                if let Some(decimals) = f.decimals {
499                    args.push(decimals);
500                }
501                Ok(Expression::Function(Box::new(Function::new(
502                    "ROUND".to_string(),
503                    args,
504                ))))
505            }
506
507            // ===== IfFunc -> IF in BigQuery =====
508            Expression::IfFunc(f) => {
509                let mut args = vec![f.condition, f.true_value];
510                if let Some(false_val) = f.false_value {
511                    args.push(false_val);
512                } else {
513                    args.push(Expression::Null(crate::expressions::Null));
514                }
515                Ok(Expression::Function(Box::new(Function::new(
516                    "IF".to_string(),
517                    args,
518                ))))
519            }
520
521            // ===== HexString -> FROM_HEX =====
522            Expression::HexStringExpr(f) => Ok(Expression::Function(Box::new(Function::new(
523                "FROM_HEX".to_string(),
524                vec![*f.this],
525            )))),
526
527            // ===== Additional auto-generated transforms from Python sqlglot =====
528            // ApproxTopK -> APPROX_TOP_COUNT
529            Expression::ApproxTopK(f) => {
530                let mut args = vec![*f.this];
531                if let Some(expr) = f.expression {
532                    args.push(*expr);
533                }
534                Ok(Expression::Function(Box::new(Function::new(
535                    "APPROX_TOP_COUNT".to_string(),
536                    args,
537                ))))
538            }
539
540            // SafeDivide -> SAFE_DIVIDE
541            Expression::SafeDivide(f) => Ok(Expression::Function(Box::new(Function::new(
542                "SAFE_DIVIDE".to_string(),
543                vec![*f.this, *f.expression],
544            )))),
545
546            // JSONKeysAtDepth -> JSON_KEYS
547            Expression::JSONKeysAtDepth(f) => Ok(Expression::Function(Box::new(Function::new(
548                "JSON_KEYS".to_string(),
549                vec![*f.this],
550            )))),
551
552            // JSONValueArray -> JSON_VALUE_ARRAY
553            Expression::JSONValueArray(f) => Ok(Expression::Function(Box::new(Function::new(
554                "JSON_VALUE_ARRAY".to_string(),
555                vec![*f.this],
556            )))),
557
558            // DateFromParts -> DATE
559            Expression::DateFromParts(f) => {
560                let mut args = Vec::new();
561                if let Some(y) = f.year {
562                    args.push(*y);
563                }
564                if let Some(m) = f.month {
565                    args.push(*m);
566                }
567                if let Some(d) = f.day {
568                    args.push(*d);
569                }
570                Ok(Expression::Function(Box::new(Function::new(
571                    "DATE".to_string(),
572                    args,
573                ))))
574            }
575
576            // SPLIT: BigQuery defaults to comma separator when none provided
577            // SPLIT(foo) -> SPLIT(foo, ',')
578            Expression::Split(f) => {
579                // Check if delimiter is empty or a placeholder - add default comma
580                let delimiter = match &f.delimiter {
581                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(s) if s.is_empty()) =>
582                    {
583                        let Literal::String(_) = lit.as_ref() else {
584                            unreachable!()
585                        };
586                        Expression::Literal(Box::new(Literal::String(",".to_string())))
587                    }
588                    _ => f.delimiter,
589                };
590                Ok(Expression::Split(Box::new(SplitFunc {
591                    this: f.this,
592                    delimiter,
593                })))
594            }
595
596            // Cast: Transform the target type according to BigQuery TYPE_MAPPING
597            // Special case: CAST to JSON -> PARSE_JSON in BigQuery
598            // Special case: CAST(x AS temporal FORMAT 'fmt') -> PARSE_DATE/PARSE_TIMESTAMP
599            Expression::Cast(c) => {
600                use crate::expressions::DataType;
601                // Check if casting to JSON - use PARSE_JSON instead
602                let is_json = matches!(c.to, DataType::Json | DataType::JsonB)
603                    || matches!(&c.to, DataType::Custom { name } if name.eq_ignore_ascii_case("JSON") || name.eq_ignore_ascii_case("JSONB"));
604                if is_json {
605                    return Ok(Expression::ParseJson(Box::new(UnaryFunc::new(c.this))));
606                }
607                // CAST(x AS temporal_type FORMAT 'fmt') -> PARSE_DATE/PARSE_TIMESTAMP(strftime_fmt, x)
608                if c.format.is_some() {
609                    let is_temporal = matches!(
610                        c.to,
611                        DataType::Date | DataType::Timestamp { .. } | DataType::Time { .. }
612                    ) || matches!(&c.to, DataType::Custom { name } if
613                        name.eq_ignore_ascii_case("TIMESTAMP") ||
614                        name.eq_ignore_ascii_case("DATE") ||
615                        name.eq_ignore_ascii_case("DATETIME") ||
616                        name.eq_ignore_ascii_case("TIME")
617                    );
618                    if is_temporal {
619                        let format_expr = c.format.as_ref().unwrap().as_ref();
620                        // Extract the actual format expr and timezone (if AT TIME ZONE is present)
621                        let (actual_format, timezone) = match format_expr {
622                            Expression::AtTimeZone(ref atz) => {
623                                (atz.this.clone(), Some(atz.zone.clone()))
624                            }
625                            _ => (format_expr.clone(), None),
626                        };
627                        let strftime_fmt = Self::bq_cast_format_to_strftime(&actual_format);
628                        let func_name = match &c.to {
629                            DataType::Date => "PARSE_DATE",
630                            DataType::Custom { name } if name.eq_ignore_ascii_case("DATE") => {
631                                "PARSE_DATE"
632                            }
633                            DataType::Custom { name } if name.eq_ignore_ascii_case("DATETIME") => {
634                                "PARSE_DATETIME"
635                            }
636                            _ => "PARSE_TIMESTAMP",
637                        };
638                        let mut func_args = vec![strftime_fmt, c.this];
639                        if let Some(tz) = timezone {
640                            func_args.push(tz);
641                        }
642                        return Ok(Expression::Function(Box::new(Function::new(
643                            func_name.to_string(),
644                            func_args,
645                        ))));
646                    }
647                }
648                let transformed_type = match self.transform_data_type(c.to)? {
649                    Expression::DataType(dt) => dt,
650                    _ => return Err(crate::error::Error::parse("Expected DataType", 0, 0, 0, 0)),
651                };
652                Ok(Expression::Cast(Box::new(crate::expressions::Cast {
653                    this: c.this,
654                    to: transformed_type,
655                    trailing_comments: c.trailing_comments,
656                    double_colon_syntax: c.double_colon_syntax,
657                    format: c.format,
658                    default: c.default,
659                    inferred_type: None,
660                })))
661            }
662
663            // SafeCast: Transform the target type according to BigQuery TYPE_MAPPING
664            Expression::SafeCast(c) => {
665                let transformed_type = match self.transform_data_type(c.to)? {
666                    Expression::DataType(dt) => dt,
667                    _ => return Err(crate::error::Error::parse("Expected DataType", 0, 0, 0, 0)),
668                };
669                Ok(Expression::SafeCast(Box::new(crate::expressions::Cast {
670                    this: c.this,
671                    to: transformed_type,
672                    trailing_comments: c.trailing_comments,
673                    double_colon_syntax: c.double_colon_syntax,
674                    format: c.format,
675                    default: c.default,
676                    inferred_type: None,
677                })))
678            }
679
680            // ===== SELECT-level transforms =====
681            // BigQuery: GROUP BY expression → alias when both GROUP BY and ORDER BY exist
682            Expression::Select(mut select) => {
683                if select.group_by.is_some() && select.order_by.is_some() {
684                    // Build map: expression → alias name for aliased projections
685                    let aliases: Vec<(Expression, Identifier)> = select
686                        .expressions
687                        .iter()
688                        .filter_map(|e| {
689                            if let Expression::Alias(a) = e {
690                                Some((a.this.clone(), a.alias.clone()))
691                            } else {
692                                None
693                            }
694                        })
695                        .collect();
696
697                    if let Some(ref mut group_by) = select.group_by {
698                        for grouped in group_by.expressions.iter_mut() {
699                            // Skip numeric indices (already aliased)
700                            if matches!(grouped, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)))
701                            {
702                                continue;
703                            }
704                            // Check if this GROUP BY expression matches a SELECT alias
705                            for (expr, alias_ident) in &aliases {
706                                if grouped == expr {
707                                    *grouped = Expression::boxed_column(Column {
708                                        name: alias_ident.clone(),
709                                        table: None,
710                                        join_mark: false,
711                                        trailing_comments: Vec::new(),
712                                        span: None,
713                                        inferred_type: None,
714                                    });
715                                    break;
716                                }
717                            }
718                        }
719                    }
720                }
721                Ok(Expression::Select(select))
722            }
723
724            // ===== ArrayContains → EXISTS(SELECT 1 FROM UNNEST(arr) AS _col WHERE _col = val) =====
725            Expression::ArrayContains(f) => {
726                let array_expr = f.this;
727                let value_expr = f.expression;
728
729                // Build: SELECT 1 FROM UNNEST(array) AS _col WHERE _col = value
730                let unnest = Expression::Unnest(Box::new(UnnestFunc {
731                    this: array_expr,
732                    expressions: Vec::new(),
733                    with_ordinality: false,
734                    alias: None,
735                    offset_alias: None,
736                    inferred_type: None,
737                }));
738                let aliased_unnest = Expression::Alias(Box::new(Alias {
739                    this: unnest,
740                    alias: Identifier::new("_col"),
741                    column_aliases: Vec::new(),
742                    alias_explicit_as: false,
743                    alias_keyword: None,
744                    pre_alias_comments: Vec::new(),
745                    trailing_comments: Vec::new(),
746                    inferred_type: None,
747                }));
748                let col_ref = Expression::boxed_column(Column {
749                    name: Identifier::new("_col"),
750                    table: None,
751                    join_mark: false,
752                    trailing_comments: Vec::new(),
753                    span: None,
754                    inferred_type: None,
755                });
756                let where_clause = Where {
757                    this: Expression::Eq(Box::new(BinaryOp {
758                        left: col_ref,
759                        right: value_expr,
760                        left_comments: Vec::new(),
761                        operator_comments: Vec::new(),
762                        trailing_comments: Vec::new(),
763                        inferred_type: None,
764                    })),
765                };
766                let inner_select = Expression::Select(Box::new(Select {
767                    vertica: None,
768                    expressions: vec![Expression::Literal(Box::new(Literal::Number(
769                        "1".to_string(),
770                    )))],
771                    from: Some(From {
772                        expressions: vec![aliased_unnest],
773                    }),
774                    where_clause: Some(where_clause),
775                    ..Default::default()
776                }));
777                Ok(Expression::Exists(Box::new(Exists {
778                    this: inner_select,
779                    not: false,
780                })))
781            }
782
783            // ===== JSON_OBJECT array form → key-value pairs =====
784            // BigQuery "signature 2": JSON_OBJECT(['a', 'b'], [10, NULL]) → JSON_OBJECT('a', 10, 'b', NULL)
785            Expression::JsonObject(mut f) => {
786                if f.pairs.len() == 1 {
787                    // Extract expressions from both Array and ArrayFunc variants
788                    let keys_exprs = match &f.pairs[0].0 {
789                        Expression::Array(arr) => Some(&arr.expressions),
790                        Expression::ArrayFunc(arr) => Some(&arr.expressions),
791                        _ => None,
792                    };
793                    let vals_exprs = match &f.pairs[0].1 {
794                        Expression::Array(arr) => Some(&arr.expressions),
795                        Expression::ArrayFunc(arr) => Some(&arr.expressions),
796                        _ => None,
797                    };
798                    if let (Some(keys), Some(vals)) = (keys_exprs, vals_exprs) {
799                        if keys.len() == vals.len() {
800                            let new_pairs: Vec<(Expression, Expression)> = keys
801                                .iter()
802                                .zip(vals.iter())
803                                .map(|(k, v)| (k.clone(), v.clone()))
804                                .collect();
805                            f.pairs = new_pairs;
806                        }
807                    }
808                }
809                Ok(Expression::JsonObject(f))
810            }
811
812            // ===== MOD function: unwrap unnecessary Paren from first argument =====
813            // BigQuery normalizes MOD((a + 1), b) -> MOD(a + 1, b)
814            Expression::ModFunc(mut f) => {
815                // Unwrap Paren from first argument if present
816                if let Expression::Paren(paren) = f.this {
817                    f.this = paren.this;
818                }
819                Ok(Expression::ModFunc(f))
820            }
821
822            // JSONExtract with variant_extract (Snowflake colon syntax) -> JSON_EXTRACT
823            Expression::JSONExtract(e) if e.variant_extract.is_some() => {
824                let path = match *e.expression {
825                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
826                        let Literal::String(s) = lit.as_ref() else {
827                            unreachable!()
828                        };
829                        let normalized = if s.starts_with('$') {
830                            s.clone()
831                        } else if s.starts_with('[') {
832                            format!("${}", s)
833                        } else {
834                            format!("$.{}", s)
835                        };
836                        Expression::Literal(Box::new(Literal::String(normalized)))
837                    }
838                    other => other,
839                };
840                Ok(Expression::Function(Box::new(Function::new(
841                    "JSON_EXTRACT".to_string(),
842                    vec![*e.this, path],
843                ))))
844            }
845
846            // Generic function transformations
847            Expression::Function(f) => self.transform_function(*f),
848
849            // Generic aggregate function transformations
850            Expression::AggregateFunction(f) => self.transform_aggregate_function(f),
851
852            // MethodCall: Handle SAFE.PARSE_DATE, SAFE.PARSE_DATETIME, SAFE.PARSE_TIMESTAMP
853            // These are parsed as MethodCall(this=SAFE, method=PARSE_DATE, args=[...])
854            Expression::MethodCall(mc) => self.transform_method_call(*mc),
855
856            // CreateFunction: Convert RETURNS TABLE(...) to RETURNS TABLE <...> for BigQuery
857            // and set is_table_function = true for TABLE FUNCTION syntax
858            Expression::CreateFunction(mut cf) => {
859                if let Some(ref mut rtb) = cf.returns_table_body {
860                    if rtb.starts_with("TABLE (") || rtb.starts_with("TABLE(") {
861                        // Convert TABLE (...) to TABLE <...> with BigQuery types
862                        let inner = if rtb.starts_with("TABLE (") {
863                            &rtb["TABLE (".len()..rtb.len() - 1]
864                        } else {
865                            &rtb["TABLE(".len()..rtb.len() - 1]
866                        };
867                        // Convert common types to BigQuery equivalents
868                        let converted = inner
869                            .replace(" INT,", " INT64,")
870                            .replace(" INT)", " INT64)")
871                            .replace(" INTEGER,", " INT64,")
872                            .replace(" INTEGER)", " INT64)")
873                            .replace(" FLOAT,", " FLOAT64,")
874                            .replace(" FLOAT)", " FLOAT64)")
875                            .replace(" BOOLEAN,", " BOOL,")
876                            .replace(" BOOLEAN)", " BOOL)")
877                            .replace(" VARCHAR", " STRING")
878                            .replace(" TEXT", " STRING");
879                        // Handle trailing type (no comma, no paren)
880                        let converted = if converted.ends_with(" INT") {
881                            format!("{}{}", &converted[..converted.len() - 4], " INT64")
882                        } else {
883                            converted
884                        };
885                        *rtb = format!("TABLE <{}>", converted);
886                        cf.is_table_function = true;
887                    }
888                }
889                // Convert string literal body to expression body for BigQuery TABLE FUNCTIONs only
890                if cf.is_table_function {
891                    if let Some(ref body) = cf.body {
892                        if matches!(body, FunctionBody::StringLiteral(_)) {
893                            if let Some(FunctionBody::StringLiteral(sql)) = cf.body.take() {
894                                // Parse the SQL string into an expression
895                                if let Ok(parsed) = crate::parser::Parser::parse_sql(&sql) {
896                                    if let Some(stmt) = parsed.into_iter().next() {
897                                        cf.body = Some(FunctionBody::Expression(stmt));
898                                    } else {
899                                        cf.body = Some(FunctionBody::StringLiteral(sql));
900                                    }
901                                } else {
902                                    cf.body = Some(FunctionBody::StringLiteral(sql));
903                                }
904                            }
905                        }
906                    }
907                }
908                Ok(Expression::CreateFunction(cf))
909            }
910
911            // Pass through everything else
912            _ => Ok(expr),
913        }
914    }
915}
916
917#[cfg(feature = "transpile")]
918impl BigQueryDialect {
919    /// Transform data types according to BigQuery TYPE_MAPPING
920    fn transform_data_type(&self, dt: crate::expressions::DataType) -> Result<Expression> {
921        use crate::expressions::DataType;
922        let transformed = match dt {
923            // BIGINT -> INT64
924            DataType::BigInt { .. } => DataType::Custom {
925                name: "INT64".to_string(),
926            },
927            // INT -> INT64
928            DataType::Int { .. } => DataType::Custom {
929                name: "INT64".to_string(),
930            },
931            // SMALLINT -> INT64
932            DataType::SmallInt { .. } => DataType::Custom {
933                name: "INT64".to_string(),
934            },
935            // TINYINT -> INT64
936            DataType::TinyInt { .. } => DataType::Custom {
937                name: "INT64".to_string(),
938            },
939            // FLOAT -> FLOAT64
940            DataType::Float { .. } => DataType::Custom {
941                name: "FLOAT64".to_string(),
942            },
943            // DOUBLE -> FLOAT64
944            DataType::Double { .. } => DataType::Custom {
945                name: "FLOAT64".to_string(),
946            },
947            // BOOLEAN -> BOOL
948            DataType::Boolean => DataType::Custom {
949                name: "BOOL".to_string(),
950            },
951            // CHAR -> STRING
952            DataType::Char { .. } => DataType::Custom {
953                name: "STRING".to_string(),
954            },
955            // VARCHAR -> STRING
956            DataType::VarChar { .. } => DataType::Custom {
957                name: "STRING".to_string(),
958            },
959            // TEXT -> STRING
960            DataType::Text => DataType::Custom {
961                name: "STRING".to_string(),
962            },
963            // STRING(n) -> STRING (BigQuery doesn't support length for STRING)
964            DataType::String { .. } => DataType::Custom {
965                name: "STRING".to_string(),
966            },
967            // BINARY -> BYTES
968            DataType::Binary { .. } => DataType::Custom {
969                name: "BYTES".to_string(),
970            },
971            // VARBINARY -> BYTES
972            DataType::VarBinary { .. } => DataType::Custom {
973                name: "BYTES".to_string(),
974            },
975            // BLOB -> BYTES
976            DataType::Blob => DataType::Custom {
977                name: "BYTES".to_string(),
978            },
979            // Preserve decimal parameters for cross-dialect conversion. The
980            // generator handles BigQuery's NUMERIC spelling and CAST restrictions.
981            decimal @ DataType::Decimal { .. } => decimal,
982            // For BigQuery identity: preserve TIMESTAMP/DATETIME as Custom types
983            // This avoids the issue where parsed TIMESTAMP (timezone: false) would
984            // be converted to DATETIME by the generator
985            DataType::Timestamp {
986                timezone: false, ..
987            } => DataType::Custom {
988                name: "TIMESTAMP".to_string(),
989            },
990            DataType::Timestamp { timezone: true, .. } => DataType::Custom {
991                name: "TIMESTAMP".to_string(),
992            },
993            // UUID -> STRING (BigQuery doesn't have native UUID type)
994            DataType::Uuid => DataType::Custom {
995                name: "STRING".to_string(),
996            },
997            // RECORD -> STRUCT in BigQuery
998            DataType::Custom { ref name } if name.eq_ignore_ascii_case("RECORD") => {
999                DataType::Custom {
1000                    name: "STRUCT".to_string(),
1001                }
1002            }
1003            // TIMESTAMPTZ (custom) -> TIMESTAMP
1004            DataType::Custom { ref name } if name.eq_ignore_ascii_case("TIMESTAMPTZ") => {
1005                DataType::Custom {
1006                    name: "TIMESTAMP".to_string(),
1007                }
1008            }
1009            // BYTEINT (custom) -> INT64
1010            DataType::Custom { ref name } if name.eq_ignore_ascii_case("BYTEINT") => {
1011                DataType::Custom {
1012                    name: "INT64".to_string(),
1013                }
1014            }
1015            // Keep all other types as-is
1016            other => other,
1017        };
1018        Ok(Expression::DataType(transformed))
1019    }
1020
1021    fn transform_function(&self, f: Function) -> Result<Expression> {
1022        let name_upper = f.name.to_uppercase();
1023        match name_upper.as_str() {
1024            // IFNULL -> COALESCE (both work in BigQuery)
1025            "IFNULL" if f.args.len() == 2 => Ok(Expression::Coalesce(Box::new(VarArgFunc {
1026                original_name: None,
1027                expressions: f.args,
1028                inferred_type: None,
1029            }))),
1030
1031            // NVL -> COALESCE
1032            "NVL" if f.args.len() == 2 => Ok(Expression::Coalesce(Box::new(VarArgFunc {
1033                original_name: None,
1034                expressions: f.args,
1035                inferred_type: None,
1036            }))),
1037
1038            // ISNULL -> COALESCE
1039            "ISNULL" if f.args.len() == 2 => Ok(Expression::Coalesce(Box::new(VarArgFunc {
1040                original_name: None,
1041                expressions: f.args,
1042                inferred_type: None,
1043            }))),
1044
1045            // GROUP_CONCAT -> STRING_AGG in BigQuery
1046            "GROUP_CONCAT" if !f.args.is_empty() => Ok(Expression::Function(Box::new(
1047                Function::new("STRING_AGG".to_string(), f.args),
1048            ))),
1049
1050            // SUBSTR -> SUBSTRING (both work)
1051            "SUBSTR" => Ok(Expression::Function(Box::new(Function::new(
1052                "SUBSTRING".to_string(),
1053                f.args,
1054            )))),
1055
1056            // RANDOM -> RAND
1057            "RANDOM" => Ok(Expression::Rand(Box::new(crate::expressions::Rand {
1058                seed: None,
1059                lower: None,
1060                upper: None,
1061            }))),
1062
1063            // CURRENT_DATE -> CURRENT_DATE() in BigQuery
1064            // Keep as Function when it has args (e.g., CURRENT_DATE('UTC'))
1065            "CURRENT_DATE" if f.args.is_empty() => {
1066                Ok(Expression::CurrentDate(crate::expressions::CurrentDate))
1067            }
1068            "CURRENT_DATE" => Ok(Expression::Function(Box::new(Function {
1069                on_error: None,
1070                qualified_name: Vec::new(),
1071                source_dialect: None,
1072
1073                name: "CURRENT_DATE".to_string(),
1074                args: f.args,
1075                distinct: false,
1076                trailing_comments: Vec::new(),
1077                use_bracket_syntax: false,
1078                no_parens: false,
1079                quoted: false,
1080                span: None,
1081                inferred_type: None,
1082            }))),
1083
1084            // NOW -> CURRENT_TIMESTAMP in BigQuery
1085            "NOW" => Ok(Expression::CurrentTimestamp(
1086                crate::expressions::CurrentTimestamp {
1087                    precision: None,
1088                    sysdate: false,
1089                },
1090            )),
1091
1092            // TO_DATE -> PARSE_DATE in BigQuery
1093            "TO_DATE" => Ok(Expression::Function(Box::new(Function::new(
1094                "PARSE_DATE".to_string(),
1095                f.args,
1096            )))),
1097
1098            // TO_TIMESTAMP -> PARSE_TIMESTAMP in BigQuery
1099            "TO_TIMESTAMP" => Ok(Expression::Function(Box::new(Function::new(
1100                "PARSE_TIMESTAMP".to_string(),
1101                f.args,
1102            )))),
1103
1104            // TO_TIME -> TIME in BigQuery
1105            "TO_TIME" if f.args.len() == 1 => Ok(Expression::Function(Box::new(Function::new(
1106                "TIME".to_string(),
1107                f.args,
1108            )))),
1109
1110            // DATE_FORMAT -> FORMAT_DATE in BigQuery (argument order may differ)
1111            "DATE_FORMAT" => Ok(Expression::Function(Box::new(Function::new(
1112                "FORMAT_DATE".to_string(),
1113                f.args,
1114            )))),
1115
1116            // POSITION -> STRPOS in BigQuery
1117            // BigQuery uses STRPOS(string, substring)
1118            "POSITION" if f.args.len() == 2 => {
1119                let mut args = f.args;
1120                // Swap arguments: POSITION(sub IN str) -> STRPOS(str, sub)
1121                let first = args.remove(0);
1122                let second = args.remove(0);
1123                Ok(Expression::Function(Box::new(Function::new(
1124                    "STRPOS".to_string(),
1125                    vec![second, first],
1126                ))))
1127            }
1128
1129            // LEN -> LENGTH
1130            "LEN" if f.args.len() == 1 => Ok(Expression::Length(Box::new(UnaryFunc::new(
1131                f.args.into_iter().next().unwrap(),
1132            )))),
1133
1134            // CEILING -> CEIL (both work)
1135            "CEILING" if f.args.len() == 1 => Ok(Expression::Ceil(Box::new(CeilFunc {
1136                this: f.args.into_iter().next().unwrap(),
1137                decimals: None,
1138                to: None,
1139            }))),
1140
1141            // GETDATE -> CURRENT_TIMESTAMP
1142            "GETDATE" => Ok(Expression::CurrentTimestamp(
1143                crate::expressions::CurrentTimestamp {
1144                    precision: None,
1145                    sysdate: false,
1146                },
1147            )),
1148
1149            // ARRAY_LENGTH -> ARRAY_LENGTH (native)
1150            // CARDINALITY -> ARRAY_LENGTH
1151            "CARDINALITY" if f.args.len() == 1 => Ok(Expression::ArrayLength(Box::new(
1152                UnaryFunc::new(f.args.into_iter().next().unwrap()),
1153            ))),
1154
1155            // UNNEST is native to BigQuery
1156
1157            // GENERATE_SERIES -> GENERATE_ARRAY in BigQuery
1158            "GENERATE_SERIES" => Ok(Expression::Function(Box::new(Function::new(
1159                "GENERATE_ARRAY".to_string(),
1160                f.args,
1161            )))),
1162
1163            // APPROX_COUNT_DISTINCT -> APPROX_COUNT_DISTINCT (native)
1164            // APPROX_DISTINCT -> APPROX_COUNT_DISTINCT
1165            "APPROX_DISTINCT" => Ok(Expression::Function(Box::new(Function::new(
1166                "APPROX_COUNT_DISTINCT".to_string(),
1167                f.args,
1168            )))),
1169
1170            // COUNT_IF -> COUNTIF in BigQuery
1171            "COUNT_IF" => Ok(Expression::Function(Box::new(Function::new(
1172                "COUNTIF".to_string(),
1173                f.args,
1174            )))),
1175
1176            // SHA1 -> SHA1 (native), SHA -> SHA1
1177            "SHA" => Ok(Expression::Function(Box::new(Function::new(
1178                "SHA1".to_string(),
1179                f.args,
1180            )))),
1181
1182            // SHA256/SHA2 -> SHA256
1183            "SHA2" => Ok(Expression::Function(Box::new(Function::new(
1184                "SHA256".to_string(),
1185                f.args,
1186            )))),
1187
1188            // MD5 in BigQuery returns bytes, often combined with TO_HEX
1189            // TO_HEX(MD5(x)) pattern
1190            "MD5" => Ok(Expression::Function(Box::new(Function::new(
1191                "MD5".to_string(),
1192                f.args,
1193            )))),
1194
1195            // VARIANCE/VAR_SAMP -> VAR_SAMP (native)
1196            // VAR_POP -> VAR_POP (native)
1197
1198            // DATEADD(unit, amount, date) → DATE_ADD(date, INTERVAL amount unit) for BigQuery
1199            "DATEADD" if f.args.len() == 3 => {
1200                let mut args = f.args;
1201                let unit_expr = args.remove(0);
1202                let amount = args.remove(0);
1203                let date = args.remove(0);
1204                // Convert unit identifier to IntervalUnit
1205                let unit_name = match &unit_expr {
1206                    Expression::Identifier(id) => id.name.to_uppercase(),
1207                    Expression::Var(v) => v.this.to_uppercase(),
1208                    Expression::Column(col) if col.table.is_none() => col.name.name.to_uppercase(),
1209                    _ => "DAY".to_string(),
1210                };
1211                let unit = match unit_name.as_str() {
1212                    "YEAR" | "YEARS" | "YY" | "YYYY" => crate::expressions::IntervalUnit::Year,
1213                    "QUARTER" | "QUARTERS" | "QQ" | "Q" => {
1214                        crate::expressions::IntervalUnit::Quarter
1215                    }
1216                    "MONTH" | "MONTHS" | "MM" | "M" => crate::expressions::IntervalUnit::Month,
1217                    "WEEK" | "WEEKS" | "WK" | "WW" => crate::expressions::IntervalUnit::Week,
1218                    "DAY" | "DAYS" | "DD" | "D" | "DAYOFMONTH" => {
1219                        crate::expressions::IntervalUnit::Day
1220                    }
1221                    "HOUR" | "HOURS" | "HH" => crate::expressions::IntervalUnit::Hour,
1222                    "MINUTE" | "MINUTES" | "MI" | "N" => crate::expressions::IntervalUnit::Minute,
1223                    "SECOND" | "SECONDS" | "SS" | "S" => crate::expressions::IntervalUnit::Second,
1224                    "MILLISECOND" | "MILLISECONDS" | "MS" => {
1225                        crate::expressions::IntervalUnit::Millisecond
1226                    }
1227                    "MICROSECOND" | "MICROSECONDS" | "US" => {
1228                        crate::expressions::IntervalUnit::Microsecond
1229                    }
1230                    _ => crate::expressions::IntervalUnit::Day,
1231                };
1232                Ok(Expression::DateAdd(Box::new(
1233                    crate::expressions::DateAddFunc {
1234                        this: date,
1235                        interval: amount,
1236                        unit,
1237                    },
1238                )))
1239            }
1240            "DATE_ADD" => Ok(Expression::Function(Box::new(Function::new(
1241                "DATE_ADD".to_string(),
1242                f.args,
1243            )))),
1244
1245            // DATE_DIFF in BigQuery (native)
1246            "DATEDIFF" => Ok(Expression::Function(Box::new(Function::new(
1247                "DATE_DIFF".to_string(),
1248                f.args,
1249            )))),
1250
1251            // TIMESTAMP_DIFF in BigQuery
1252            "TIMESTAMPDIFF" => Ok(Expression::Function(Box::new(Function::new(
1253                "TIMESTAMP_DIFF".to_string(),
1254                f.args,
1255            )))),
1256
1257            // TIME -> TIME (native)
1258            // DATETIME -> DATETIME (native)
1259
1260            // SAFE_DIVIDE -> SAFE_DIVIDE (native)
1261
1262            // NEWID/UUID -> GENERATE_UUID
1263            "NEWID" | "UUID" => Ok(Expression::Function(Box::new(Function::new(
1264                "GENERATE_UUID".to_string(),
1265                vec![],
1266            )))),
1267
1268            // LEVENSHTEIN -> EDIT_DISTANCE (BigQuery naming)
1269            "LEVENSHTEIN" => Ok(Expression::Function(Box::new(Function::new(
1270                "EDIT_DISTANCE".to_string(),
1271                f.args,
1272            )))),
1273
1274            // UNIX_TIMESTAMP -> UNIX_SECONDS
1275            "UNIX_TIMESTAMP" => Ok(Expression::Function(Box::new(Function::new(
1276                "UNIX_SECONDS".to_string(),
1277                f.args,
1278            )))),
1279
1280            // FROM_UNIXTIME -> TIMESTAMP_SECONDS
1281            "FROM_UNIXTIME" => Ok(Expression::Function(Box::new(Function::new(
1282                "TIMESTAMP_SECONDS".to_string(),
1283                f.args,
1284            )))),
1285
1286            // CHAR_LENGTH / CHARACTER_LENGTH -> LENGTH
1287            "CHAR_LENGTH" | "CHARACTER_LENGTH" => Ok(Expression::Function(Box::new(
1288                Function::new("LENGTH".to_string(), f.args),
1289            ))),
1290
1291            // OCTET_LENGTH -> BYTE_LENGTH in BigQuery
1292            "OCTET_LENGTH" => Ok(Expression::Function(Box::new(Function::new(
1293                "BYTE_LENGTH".to_string(),
1294                f.args,
1295            )))),
1296
1297            // JSON_EXTRACT_STRING_ARRAY -> JSON_VALUE_ARRAY in BigQuery
1298            "JSON_EXTRACT_STRING_ARRAY" => Ok(Expression::Function(Box::new(Function::new(
1299                "JSON_VALUE_ARRAY".to_string(),
1300                f.args,
1301            )))),
1302
1303            // INSTR is native to BigQuery
1304
1305            // SPLIT: BigQuery defaults to comma separator when none provided
1306            // SPLIT(foo) -> SPLIT(foo, ',')
1307            "SPLIT" if f.args.len() == 1 => {
1308                let mut args = f.args;
1309                args.push(Expression::Literal(Box::new(Literal::String(
1310                    ",".to_string(),
1311                ))));
1312                Ok(Expression::Split(Box::new(SplitFunc {
1313                    this: args.remove(0),
1314                    delimiter: args.remove(0),
1315                })))
1316            }
1317
1318            // SPLIT with two args - convert to Split expression
1319            "SPLIT" if f.args.len() == 2 => {
1320                let mut args = f.args;
1321                Ok(Expression::Split(Box::new(SplitFunc {
1322                    this: args.remove(0),
1323                    delimiter: args.remove(0),
1324                })))
1325            }
1326
1327            // REGEXP_SUBSTR -> REGEXP_EXTRACT in BigQuery (strip extra Snowflake args)
1328            "REGEXP_SUBSTR" if f.args.len() >= 2 => {
1329                // BigQuery REGEXP_EXTRACT supports (subject, pattern, pos, occ) max 4 args
1330                let args = if f.args.len() > 4 {
1331                    f.args[..4].to_vec()
1332                } else {
1333                    f.args
1334                };
1335                Ok(Expression::Function(Box::new(Function::new(
1336                    "REGEXP_EXTRACT".to_string(),
1337                    args,
1338                ))))
1339            }
1340            "REGEXP_SUBSTR" => Ok(Expression::Function(Box::new(Function::new(
1341                "REGEXP_EXTRACT".to_string(),
1342                f.args,
1343            )))),
1344
1345            // REGEXP_REPLACE - strip extra Snowflake-specific args
1346            "REGEXP_REPLACE" if f.args.len() > 3 => {
1347                let args = f.args[..3].to_vec();
1348                Ok(Expression::Function(Box::new(Function::new(
1349                    "REGEXP_REPLACE".to_string(),
1350                    args,
1351                ))))
1352            }
1353
1354            // OBJECT_CONSTRUCT_KEEP_NULL -> JSON_OBJECT
1355            "OBJECT_CONSTRUCT_KEEP_NULL" => Ok(Expression::Function(Box::new(Function::new(
1356                "JSON_OBJECT".to_string(),
1357                f.args,
1358            )))),
1359
1360            // EDITDISTANCE -> EDIT_DISTANCE with named max_distance parameter
1361            "EDITDISTANCE" if f.args.len() == 3 => {
1362                let col1 = f.args[0].clone();
1363                let col2 = f.args[1].clone();
1364                let max_dist = f.args[2].clone();
1365                Ok(Expression::Function(Box::new(Function::new(
1366                    "EDIT_DISTANCE".to_string(),
1367                    vec![
1368                        col1,
1369                        col2,
1370                        Expression::NamedArgument(Box::new(crate::expressions::NamedArgument {
1371                            name: crate::expressions::Identifier::new("max_distance".to_string()),
1372                            value: max_dist,
1373                            separator: crate::expressions::NamedArgSeparator::DArrow,
1374                        })),
1375                    ],
1376                ))))
1377            }
1378            "EDITDISTANCE" if f.args.len() == 2 => Ok(Expression::Function(Box::new(
1379                Function::new("EDIT_DISTANCE".to_string(), f.args),
1380            ))),
1381
1382            // HEX_DECODE_BINARY -> FROM_HEX
1383            "HEX_DECODE_BINARY" => Ok(Expression::Function(Box::new(Function::new(
1384                "FROM_HEX".to_string(),
1385                f.args,
1386            )))),
1387
1388            // BigQuery format string normalization for PARSE_DATE/DATETIME/TIMESTAMP functions
1389            // %Y-%m-%d -> %F and %H:%M:%S -> %T
1390            "PARSE_DATE"
1391            | "PARSE_DATETIME"
1392            | "PARSE_TIMESTAMP"
1393            | "SAFE.PARSE_DATE"
1394            | "SAFE.PARSE_DATETIME"
1395            | "SAFE.PARSE_TIMESTAMP" => {
1396                let args = self.normalize_time_format_args(f.args);
1397                Ok(Expression::Function(Box::new(Function {
1398                    on_error: None,
1399                    qualified_name: Vec::new(),
1400                    source_dialect: None,
1401
1402                    name: f.name,
1403                    args,
1404                    distinct: f.distinct,
1405                    no_parens: f.no_parens,
1406                    trailing_comments: f.trailing_comments,
1407                    quoted: f.quoted,
1408                    use_bracket_syntax: f.use_bracket_syntax,
1409                    span: None,
1410                    inferred_type: None,
1411                })))
1412            }
1413
1414            // GET_PATH(obj, path) -> JSON_EXTRACT(obj, json_path) in BigQuery
1415            "GET_PATH" if f.args.len() == 2 => {
1416                let mut args = f.args;
1417                let this = args.remove(0);
1418                let path = args.remove(0);
1419                let json_path = match &path {
1420                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
1421                        let Literal::String(s) = lit.as_ref() else {
1422                            unreachable!()
1423                        };
1424                        let normalized = if s.starts_with('$') {
1425                            s.clone()
1426                        } else if s.starts_with('[') {
1427                            format!("${}", s)
1428                        } else {
1429                            format!("$.{}", s)
1430                        };
1431                        Expression::Literal(Box::new(Literal::String(normalized)))
1432                    }
1433                    _ => path,
1434                };
1435                Ok(Expression::JsonExtract(Box::new(JsonExtractFunc {
1436                    options: None,
1437                    source_dialect: None,
1438
1439                    this,
1440                    path: json_path,
1441                    returning: None,
1442                    arrow_syntax: false,
1443                    hash_arrow_syntax: false,
1444                    wrapper_option: None,
1445                    quotes_option: None,
1446                    on_scalar_string: false,
1447                    on_error: None,
1448                })))
1449            }
1450
1451            // Pass through everything else
1452            _ => Ok(Expression::Function(Box::new(f))),
1453        }
1454    }
1455
1456    fn transform_aggregate_function(
1457        &self,
1458        f: Box<crate::expressions::AggregateFunction>,
1459    ) -> Result<Expression> {
1460        let name_upper = f.name.to_uppercase();
1461        match name_upper.as_str() {
1462            // GROUP_CONCAT -> STRING_AGG
1463            "GROUP_CONCAT" if !f.args.is_empty() => Ok(Expression::Function(Box::new(
1464                Function::new("STRING_AGG".to_string(), f.args),
1465            ))),
1466
1467            // Pass through everything else
1468            _ => Ok(Expression::AggregateFunction(f)),
1469        }
1470    }
1471
1472    /// Transform MethodCall expressions
1473    /// Handles SAFE.PARSE_DATE, SAFE.PARSE_DATETIME, SAFE.PARSE_TIMESTAMP format normalization
1474    fn transform_method_call(&self, mc: crate::expressions::MethodCall) -> Result<Expression> {
1475        use crate::expressions::MethodCall;
1476
1477        // Check if this is SAFE.PARSE_DATE/DATETIME/TIMESTAMP
1478        if let Expression::Column(ref col) = mc.this {
1479            if col.name.name.eq_ignore_ascii_case("SAFE") {
1480                let method_upper = mc.method.name.to_uppercase();
1481                if method_upper == "PARSE_DATE"
1482                    || method_upper == "PARSE_DATETIME"
1483                    || method_upper == "PARSE_TIMESTAMP"
1484                {
1485                    // Normalize the format string in the first argument
1486                    let args = self.normalize_time_format_args(mc.args);
1487                    return Ok(Expression::MethodCall(Box::new(MethodCall {
1488                        this: mc.this,
1489                        method: mc.method,
1490                        args,
1491                    })));
1492                }
1493            }
1494        }
1495
1496        // Pass through all other method calls
1497        Ok(Expression::MethodCall(Box::new(mc)))
1498    }
1499
1500    /// Normalize time format strings in function arguments
1501    /// BigQuery normalizes: %Y-%m-%d -> %F, %H:%M:%S -> %T
1502    fn normalize_time_format_args(&self, args: Vec<Expression>) -> Vec<Expression> {
1503        args.into_iter()
1504            .enumerate()
1505            .map(|(i, arg)| {
1506                // Only transform the first argument (the format string)
1507                if i == 0 {
1508                    if let Expression::Literal(ref lit) = arg {
1509                        if let Literal::String(s) = lit.as_ref() {
1510                            let normalized = self.normalize_time_format(&s);
1511                            return Expression::Literal(Box::new(Literal::String(normalized)));
1512                        }
1513                    }
1514                }
1515                arg
1516            })
1517            .collect()
1518    }
1519
1520    /// Normalize a time format string according to BigQuery conventions
1521    /// %Y-%m-%d -> %F (ISO date)
1522    /// %H:%M:%S -> %T (time)
1523    fn normalize_time_format(&self, format: &str) -> String {
1524        format.replace("%Y-%m-%d", "%F").replace("%H:%M:%S", "%T")
1525    }
1526
1527    /// Convert BigQuery CAST FORMAT elements to strftime equivalents,
1528    /// then normalize BigQuery shorthand forms (%Y-%m-%d -> %F, %H:%M:%S -> %T)
1529    fn bq_cast_format_to_strftime(format_expr: &Expression) -> Expression {
1530        use crate::expressions::Literal;
1531        if let Expression::Literal(lit) = format_expr {
1532            if let Literal::String(s) = lit.as_ref() {
1533                let result = s
1534                    .replace("YYYYMMDD", "%Y%m%d")
1535                    .replace("YYYY", "%Y")
1536                    .replace("YY", "%y")
1537                    .replace("MONTH", "%B")
1538                    .replace("MON", "%b")
1539                    .replace("MM", "%m")
1540                    .replace("DD", "%d")
1541                    .replace("HH24", "%H")
1542                    .replace("HH12", "%I")
1543                    .replace("HH", "%I")
1544                    .replace("MI", "%M")
1545                    .replace("SSTZH", "%S%z")
1546                    .replace("SS", "%S")
1547                    .replace("TZH", "%z");
1548                // Normalize: %Y-%m-%d -> %F, %H:%M:%S -> %T
1549                let normalized = result.replace("%Y-%m-%d", "%F").replace("%H:%M:%S", "%T");
1550                return Expression::Literal(Box::new(Literal::String(normalized)));
1551            }
1552        }
1553        format_expr.clone()
1554    }
1555}
1556
1557#[cfg(test)]
1558mod tests {
1559    use super::*;
1560    use crate::dialects::Dialect;
1561    use crate::parse_one;
1562
1563    fn transpile_to_bigquery(sql: &str) -> String {
1564        let dialect = Dialect::get(DialectType::Generic);
1565        let result = dialect
1566            .transpile(sql, DialectType::BigQuery)
1567            .expect("Transpile failed");
1568        result[0].clone()
1569    }
1570
1571    #[test]
1572    fn test_ifnull_identity() {
1573        // Generic -> BigQuery: IFNULL is normalized to COALESCE (matching sqlglot behavior)
1574        let result = transpile_to_bigquery("SELECT IFNULL(a, b)");
1575        assert!(
1576            result.contains("COALESCE"),
1577            "Expected COALESCE, got: {}",
1578            result
1579        );
1580    }
1581
1582    #[test]
1583    fn test_nvl_to_ifnull() {
1584        // NVL is converted to IFNULL in BigQuery
1585        let result = transpile_to_bigquery("SELECT NVL(a, b)");
1586        assert!(
1587            result.contains("IFNULL"),
1588            "Expected IFNULL, got: {}",
1589            result
1590        );
1591    }
1592
1593    #[test]
1594    fn test_try_cast_to_safe_cast() {
1595        let result = transpile_to_bigquery("SELECT TRY_CAST(a AS INT)");
1596        assert!(
1597            result.contains("SAFE_CAST"),
1598            "Expected SAFE_CAST, got: {}",
1599            result
1600        );
1601    }
1602
1603    #[test]
1604    fn test_random_to_rand() {
1605        let result = transpile_to_bigquery("SELECT RANDOM()");
1606        assert!(result.contains("RAND"), "Expected RAND, got: {}", result);
1607    }
1608
1609    #[test]
1610    fn test_basic_select() {
1611        let result = transpile_to_bigquery("SELECT a, b FROM users WHERE id = 1");
1612        assert!(result.contains("SELECT"));
1613        assert!(result.contains("FROM users"));
1614    }
1615
1616    #[test]
1617    fn test_group_concat_to_string_agg() {
1618        let result = transpile_to_bigquery("SELECT GROUP_CONCAT(name)");
1619        assert!(
1620            result.contains("STRING_AGG"),
1621            "Expected STRING_AGG, got: {}",
1622            result
1623        );
1624    }
1625
1626    #[test]
1627    fn test_generate_series_to_generate_array() {
1628        let result = transpile_to_bigquery("SELECT GENERATE_SERIES(1, 10)");
1629        assert!(
1630            result.contains("GENERATE_ARRAY"),
1631            "Expected GENERATE_ARRAY, got: {}",
1632            result
1633        );
1634    }
1635
1636    #[test]
1637    fn test_backtick_identifiers() {
1638        // BigQuery uses backticks for identifiers
1639        let dialect = BigQueryDialect;
1640        let config = dialect.generator_config();
1641        assert_eq!(config.identifier_quote, '`');
1642    }
1643
1644    fn bigquery_identity(sql: &str, expected: &str) {
1645        let dialect = Dialect::get(DialectType::BigQuery);
1646        let ast = dialect.parse(sql).expect("Parse failed");
1647        let transformed = dialect.transform(ast[0].clone()).expect("Transform failed");
1648        let result = dialect.generate(&transformed).expect("Generate failed");
1649        assert_eq!(result, expected, "SQL: {}", sql);
1650    }
1651
1652    #[test]
1653    fn test_numeric_parameters_survive_transformation_479() {
1654        use crate::expressions::DataType;
1655        use crate::traversal::ExpressionWalk;
1656
1657        let dialect = Dialect::get(DialectType::BigQuery);
1658        let ast = parse_one(
1659            "SELECT CAST(1.005 AS NUMERIC(10, 2))",
1660            DialectType::BigQuery,
1661        )
1662        .unwrap();
1663        let transformed = dialect.transform(ast).unwrap();
1664        assert!(transformed.dfs().any(|node| matches!(node,
1665            Expression::Cast(cast) if matches!(cast.to, DataType::Decimal { precision: Some(10), scale: Some(2) })
1666        )));
1667        assert_eq!(
1668            dialect.generate(&transformed).unwrap(),
1669            "SELECT CAST(1.005 AS NUMERIC)"
1670        );
1671        for (sql, expected) in [
1672            (
1673                "SELECT SAFE_CAST(1 AS NUMERIC(10, 2))",
1674                "SELECT SAFE_CAST(1 AS NUMERIC)",
1675            ),
1676            (
1677                "SELECT CAST(a AS ARRAY<NUMERIC(10, 2)>)",
1678                "SELECT CAST(a AS ARRAY<NUMERIC>)",
1679            ),
1680            (
1681                "CREATE TABLE t (a NUMERIC(10, 2), b NUMERIC)",
1682                "CREATE TABLE t (a NUMERIC(10, 2), b NUMERIC)",
1683            ),
1684        ] {
1685            bigquery_identity(sql, expected);
1686        }
1687    }
1688
1689    #[test]
1690    fn test_named_query_parameters_478() {
1691        use crate::expressions::ParameterStyle;
1692        use crate::traversal::ExpressionWalk;
1693
1694        for (sql, name, quoted) in [
1695            ("SELECT @sites", "sites", false),
1696            ("SELECT @Sites_1", "Sites_1", false),
1697            ("SELECT @select", "select", false),
1698            ("SELECT @`a b`", "a b", true),
1699            ("SELECT @param.dataField", "param", false),
1700            ("SELECT @ids[OFFSET(0)]", "ids", false),
1701            ("SELECT @sites + 1", "sites", false),
1702        ] {
1703            let ast = parse_one(sql, DialectType::BigQuery).unwrap();
1704            let parameters: Vec<_> = ast
1705                .dfs()
1706                .filter_map(|node| match node {
1707                    Expression::Parameter(parameter) => Some(parameter),
1708                    _ => None,
1709                })
1710                .collect();
1711            assert_eq!(parameters.len(), 1, "{sql}");
1712            assert_eq!(parameters[0].name.as_deref(), Some(name), "{sql}");
1713            assert_eq!(parameters[0].style, ParameterStyle::At, "{sql}");
1714            assert_eq!(parameters[0].quoted, quoted, "{sql}");
1715            assert!(
1716                !ast.dfs().any(|node| match node {
1717                    Expression::Abs(_) => true,
1718                    Expression::Column(column) => column.name.name.starts_with('@'),
1719                    _ => false,
1720                }),
1721                "{sql}"
1722            );
1723            bigquery_identity(sql, sql);
1724        }
1725        bigquery_identity("SELECT @ sites", "SELECT @sites");
1726        bigquery_identity("SELECT @@time_zone", "SELECT @@time_zone");
1727        bigquery_identity("SELECT ?", "SELECT ?");
1728        for sql in [
1729            "SELECT @",
1730            "SELECT @1",
1731            "SELECT @-1",
1732            "SELECT @(1)",
1733            "SELECT @'name'",
1734            "SELECT @foo$bar",
1735        ] {
1736            assert!(parse_one(sql, DialectType::BigQuery).is_err(), "{sql}");
1737        }
1738    }
1739
1740    #[test]
1741    fn test_safe_namespace_parses_as_function() {
1742        let expr = parse_one(
1743            "SELECT SAFE.PARSE_JSON(data) AS json_data FROM t",
1744            DialectType::BigQuery,
1745        )
1746        .expect("parse");
1747
1748        let Expression::Select(select) = expr else {
1749            panic!("expected SELECT");
1750        };
1751        let Expression::Alias(alias) = &select.expressions[0] else {
1752            panic!("expected alias");
1753        };
1754        let Expression::Function(function) = &alias.this else {
1755            panic!("expected SAFE namespace call to parse as Function");
1756        };
1757
1758        assert_eq!(function.name, "SAFE.PARSE_JSON");
1759        assert_eq!(function.args.len(), 1);
1760    }
1761
1762    #[test]
1763    fn test_safe_namespace_identity() {
1764        bigquery_identity("SAFE.PARSE_JSON(data)", "SAFE.PARSE_JSON(data)");
1765        bigquery_identity(
1766            "SAFE.PARSE_DATE('%Y-%m-%d', date_col)",
1767            "SAFE.PARSE_DATE('%F', date_col)",
1768        );
1769        bigquery_identity("SAFE.DIVIDE(a, b)", "SAFE.DIVIDE(a, b)");
1770    }
1771
1772    #[test]
1773    fn test_cast_char_to_string() {
1774        bigquery_identity("CAST(x AS CHAR)", "CAST(x AS STRING)");
1775    }
1776
1777    #[test]
1778    fn test_cast_varchar_to_string() {
1779        bigquery_identity("CAST(x AS VARCHAR)", "CAST(x AS STRING)");
1780    }
1781
1782    #[test]
1783    fn test_cast_nchar_to_string() {
1784        bigquery_identity("CAST(x AS NCHAR)", "CAST(x AS STRING)");
1785    }
1786
1787    #[test]
1788    fn test_cast_nvarchar_to_string() {
1789        bigquery_identity("CAST(x AS NVARCHAR)", "CAST(x AS STRING)");
1790    }
1791
1792    #[test]
1793    fn test_cast_timestamptz_to_timestamp() {
1794        bigquery_identity("CAST(x AS TIMESTAMPTZ)", "CAST(x AS TIMESTAMP)");
1795    }
1796
1797    #[test]
1798    fn test_cast_record_to_struct() {
1799        bigquery_identity("CAST(x AS RECORD)", "CAST(x AS STRUCT)");
1800    }
1801
1802    #[test]
1803    fn test_json_literal_to_parse_json() {
1804        // JSON 'string' literal syntax should be converted to PARSE_JSON()
1805        bigquery_identity(
1806            "SELECT JSON '\"foo\"' AS json_data",
1807            "SELECT PARSE_JSON('\"foo\"') AS json_data",
1808        );
1809    }
1810
1811    #[test]
1812    fn test_grant_as_alias_not_quoted() {
1813        // GRANT is not a reserved keyword in BigQuery, should not be backtick-quoted
1814        bigquery_identity(
1815            "SELECT GRANT FROM (SELECT 'input' AS GRANT)",
1816            "SELECT GRANT FROM (SELECT 'input' AS GRANT)",
1817        );
1818    }
1819
1820    #[test]
1821    fn test_timestamp_literal_to_cast() {
1822        // TIMESTAMP 'value' literal should be converted to CAST('value' AS TIMESTAMP)
1823        bigquery_identity(
1824            "CREATE VIEW `d.v` OPTIONS (expiration_timestamp=TIMESTAMP '2020-01-02T04:05:06.007Z') AS SELECT 1 AS c",
1825            "CREATE VIEW `d.v` OPTIONS (expiration_timestamp=CAST('2020-01-02T04:05:06.007Z' AS TIMESTAMP)) AS SELECT 1 AS c"
1826        );
1827    }
1828
1829    #[test]
1830    fn test_date_literal_to_cast_in_extract() {
1831        // Issue 1: DATE literal should become CAST syntax in BigQuery
1832        bigquery_identity(
1833            "EXTRACT(WEEK(THURSDAY) FROM DATE '2013-12-25')",
1834            "EXTRACT(WEEK(THURSDAY) FROM CAST('2013-12-25' AS DATE))",
1835        );
1836    }
1837
1838    #[test]
1839    fn test_json_object_with_json_literals() {
1840        // Issue 2: JSON literals in JSON_OBJECT should use PARSE_JSON, not CAST AS JSON
1841        bigquery_identity(
1842            "SELECT JSON_OBJECT('a', JSON '10') AS json_data",
1843            "SELECT JSON_OBJECT('a', PARSE_JSON('10')) AS json_data",
1844        );
1845    }
1846
1847    // NOTE: MOD paren unwrapping is tested in the conformance tests (sqlglot_dialect_identity).
1848    // The unit test version was removed due to stack overflow in debug builds (deep recursion).
1849    // Test case: MOD((a + 1), b) -> MOD(a + 1, b)
1850
1851    #[test]
1852    fn test_safe_parse_date_format_normalization() {
1853        // SAFE.PARSE_DATE format string normalization: %Y-%m-%d -> %F
1854        bigquery_identity(
1855            "SAFE.PARSE_DATE('%Y-%m-%d', '2024-01-15')",
1856            "SAFE.PARSE_DATE('%F', '2024-01-15')",
1857        );
1858    }
1859
1860    #[test]
1861    fn test_safe_parse_datetime_format_normalization() {
1862        // SAFE.PARSE_DATETIME format string normalization: %Y-%m-%d %H:%M:%S -> %F %T
1863        bigquery_identity(
1864            "SAFE.PARSE_DATETIME('%Y-%m-%d %H:%M:%S', '2024-01-15 10:30:00')",
1865            "SAFE.PARSE_DATETIME('%F %T', '2024-01-15 10:30:00')",
1866        );
1867    }
1868
1869    #[test]
1870    fn test_safe_parse_timestamp_format_normalization() {
1871        // SAFE.PARSE_TIMESTAMP format string normalization: %Y-%m-%d %H:%M:%S -> %F %T
1872        bigquery_identity(
1873            "SAFE.PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '2024-01-15 10:30:00')",
1874            "SAFE.PARSE_TIMESTAMP('%F %T', '2024-01-15 10:30:00')",
1875        );
1876    }
1877
1878    #[test]
1879    fn test_datetime_literal_to_cast() {
1880        // DATETIME 'value' literal should be converted to CAST('value' AS DATETIME)
1881        bigquery_identity(
1882            "LAST_DAY(DATETIME '2008-11-10 15:30:00', WEEK(SUNDAY))",
1883            "LAST_DAY(CAST('2008-11-10 15:30:00' AS DATETIME), WEEK)",
1884        );
1885    }
1886
1887    #[test]
1888    fn test_last_day_week_modifier_stripped() {
1889        // WEEK(SUNDAY) should become WEEK in BigQuery LAST_DAY function
1890        bigquery_identity("LAST_DAY(col, WEEK(MONDAY))", "LAST_DAY(col, WEEK)");
1891    }
1892
1893    #[test]
1894    fn test_hash_line_comment_parses() {
1895        // Regression test for issue #38:
1896        // BigQuery should accept # as a single-line comment.
1897        let result = parse_one("SELECT 1 as a #hello world", DialectType::BigQuery);
1898        assert!(result.is_ok(), "Expected parse to succeed, got: {result:?}");
1899    }
1900}