Edit on GitHub

sqlglot.generators.duckdb

   1from __future__ import annotations
   2
   3from decimal import Decimal
   4from itertools import groupby
   5import re
   6import typing as t
   7
   8from sqlglot import exp, generator, transforms
   9from sqlglot.dialects.dialect import (
  10    DATETIME_DELTA,
  11    JSON_EXTRACT_TYPE,
  12    approx_count_distinct_sql,
  13    array_append_sql,
  14    array_compact_sql,
  15    array_concat_sql,
  16    arrow_json_extract_sql,
  17    count_if_to_sum,
  18    date_delta_to_binary_interval_op,
  19    datestrtodate_sql,
  20    encode_decode_sql,
  21    explode_to_unnest_sql,
  22    generate_series_sql,
  23    getbit_sql,
  24    groupconcat_sql,
  25    inline_array_unless_query,
  26    months_between_sql,
  27    no_datetime_sql,
  28    no_comment_column_constraint_sql,
  29    no_make_interval_sql,
  30    no_time_sql,
  31    no_timestamp_sql,
  32    rename_func,
  33    remove_from_array_using_filter,
  34    strposition_sql,
  35    timestrtotime_sql,
  36    unit_to_str,
  37    week_unit_to_dow,
  38    weekstart_unit_to_str,
  39    WEEK_START_DAY_TO_DOW,
  40)
  41from sqlglot.generator import unsupported_args
  42from sqlglot.helper import find_new_name, is_date_unit, seq_get
  43from sqlglot.optimizer.scope import find_all_in_scope
  44from builtins import type as Type
  45
  46_CONNECT_BY_ARGS_TO_SKIP = frozenset({"connect", "where", "from_", "with_", "expressions"})
  47
  48# Regex to detect time zones in timestamps of the form [+|-]TT[:tt]
  49# The pattern matches timezone offsets that appear after the time portion
  50TIMEZONE_PATTERN = re.compile(r":\d{2}.*?[+\-]\d{2}(?::\d{2})?")
  51
  52# Characters that must be escaped when building regex expressions in INITCAP
  53REGEX_ESCAPE_REPLACEMENTS = {
  54    "\\": "\\\\",
  55    "-": r"\-",
  56    "^": r"\^",
  57    "[": r"\[",
  58    "]": r"\]",
  59}
  60
  61# Used to in RANDSTR transpilation
  62RANDSTR_CHAR_POOL = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
  63RANDSTR_SEED = 123456
  64
  65# Whitespace control characters that DuckDB must process with `CHR({val})` calls
  66WS_CONTROL_CHARS_TO_DUCK = {
  67    "\u000b": 11,
  68    "\u001c": 28,
  69    "\u001d": 29,
  70    "\u001e": 30,
  71    "\u001f": 31,
  72}
  73
  74MAX_BIT_POSITION = exp.Literal.number(32768)
  75
  76# cs/as/ps are Snowflake defaults; DuckDB already behaves the same way, so they are safe to drop.
  77# Note: "as" is also a reserved keyword in DuckDB, making it impossible to pass through.
  78_SNOWFLAKE_COLLATION_DEFAULTS = frozenset({"cs", "as", "ps"})
  79_SNOWFLAKE_COLLATION_UNSUPPORTED = frozenset(
  80    {"ci", "ai", "upper", "lower", "utf8", "bin", "pi", "fl", "fu", "trim", "ltrim", "rtrim"}
  81)
  82
  83# Window functions that support IGNORE/RESPECT NULLS in DuckDB
  84_IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS = (
  85    exp.FirstValue,
  86    exp.Lag,
  87    exp.LastValue,
  88    exp.Lead,
  89    exp.NthValue,
  90)
  91
  92# SEQ function constants
  93_SEQ_BASE: exp.Expr = exp.maybe_parse("(ROW_NUMBER() OVER (ORDER BY 1) - 1)")
  94_SEQ_RESTRICTED = (exp.Where, exp.Having, exp.AggFunc, exp.Order, exp.Select)
  95# Maps SEQ expression types to their byte width (suffix indicates bytes: SEQ1=1, SEQ2=2, etc.)
  96_SEQ_BYTE_WIDTH = {exp.Seq1: 1, exp.Seq2: 2, exp.Seq4: 4, exp.Seq8: 8}
  97
  98# Template for generating signed and unsigned SEQ values within a specified range
  99_SEQ_UNSIGNED: exp.Expr = exp.maybe_parse(":base % :max_val")
 100_SEQ_SIGNED: exp.Expr = exp.maybe_parse(
 101    "(CASE WHEN :base % :max_val >= :half "
 102    "THEN :base % :max_val - :max_val "
 103    "ELSE :base % :max_val END)"
 104)
 105
 106
 107def _apply_base64_alphabet_replacements(
 108    result: exp.Expr,
 109    alphabet: exp.Expr | None,
 110    reverse: bool = False,
 111) -> exp.Expr:
 112    """
 113    Apply base64 alphabet character replacements.
 114
 115    Base64 alphabet can be 1-3 chars: 1st = index 62 ('+'), 2nd = index 63 ('/'), 3rd = padding ('=').
 116    zip truncates to the shorter string, so 1-char alphabet only replaces '+', 2-char replaces '+/', etc.
 117
 118    Args:
 119        result: The expression to apply replacements to
 120        alphabet: Custom alphabet literal (expected chars for +/=)
 121        reverse: If False, replace default with custom (encode)
 122                 If True, replace custom with default (decode)
 123    """
 124    if isinstance(alphabet, exp.Literal) and alphabet.is_string:
 125        for default_char, new_char in zip("+/=", alphabet.this):
 126            if new_char != default_char:
 127                find, replace = (new_char, default_char) if reverse else (default_char, new_char)
 128                result = exp.Replace(
 129                    this=result,
 130                    expression=exp.Literal.string(find),
 131                    replacement=exp.Literal.string(replace),
 132                )
 133    return result
 134
 135
 136def _base64_decode_sql(self: DuckDBGenerator, expression: exp.Expr, to_string: bool) -> str:
 137    """
 138    Transpile Snowflake BASE64_DECODE_STRING/BINARY to DuckDB.
 139
 140    DuckDB uses FROM_BASE64() which returns BLOB. For string output, wrap with DECODE().
 141    Custom alphabets require REPLACE() calls to convert to standard base64.
 142    """
 143    input_expr = expression.this
 144    alphabet = expression.args.get("alphabet")
 145
 146    # Handle custom alphabet by replacing non-standard chars with standard ones
 147    input_expr = _apply_base64_alphabet_replacements(input_expr, alphabet, reverse=True)
 148
 149    # FROM_BASE64 returns BLOB
 150    input_expr = exp.FromBase64(this=input_expr)
 151
 152    if to_string:
 153        input_expr = exp.Decode(this=input_expr)
 154
 155    return self.sql(input_expr)
 156
 157
 158def _last_day_sql(self: DuckDBGenerator, expression: exp.LastDay) -> str:
 159    """
 160    DuckDB's LAST_DAY only supports finding the last day of a month.
 161    For other date parts (year, quarter, week), we need to implement equivalent logic.
 162    """
 163    date_expr = expression.this
 164    unit_expr = expression.args.get("unit")
 165
 166    week_start = week_unit_to_dow(unit_expr)
 167    if week_start:
 168        # The week's last day precedes its start day; DuckDB DAYOFWEEK: Sunday=0, ..., Saturday=6
 169        last_dow = week_start - 1
 170        dow = exp.func("EXTRACT", "DAYOFWEEK", date_expr)
 171
 172        # Days to the last day of week: (last_dow + 7 - dayofweek) % 7
 173        days_to_last_expr = exp.Mod(
 174            this=exp.Paren(this=exp.Sub(this=exp.Literal.number(last_dow + 7), expression=dow)),
 175            expression=exp.Literal.number(7),
 176        )
 177        interval_expr = exp.Interval(this=days_to_last_expr, unit=exp.var("DAY"))
 178        add_expr = exp.Add(this=date_expr, expression=interval_expr)
 179
 180        return self.sql(exp.cast(add_expr, exp.DType.DATE))
 181
 182    unit = expression.text("unit")
 183
 184    if not unit or unit.upper() == "MONTH":
 185        # Default behavior - use DuckDB's native LAST_DAY
 186        return self.func("LAST_DAY", date_expr)
 187
 188    if unit.upper() == "YEAR":
 189        # Last day of year: December 31st of the same year
 190        year_expr = exp.func("EXTRACT", "YEAR", date_expr)
 191        make_date_expr = exp.func(
 192            "MAKE_DATE", year_expr, exp.Literal.number(12), exp.Literal.number(31)
 193        )
 194        return self.sql(make_date_expr)
 195
 196    if unit.upper() == "QUARTER":
 197        # Last day of quarter
 198        year_expr = exp.func("EXTRACT", "YEAR", date_expr)
 199        quarter_expr = exp.func("EXTRACT", "QUARTER", date_expr)
 200
 201        # Calculate last month of quarter: quarter * 3. Quarter can be 1 to 4
 202        last_month_expr = exp.Mul(this=quarter_expr, expression=exp.Literal.number(3))
 203        first_day_last_month_expr = exp.func(
 204            "MAKE_DATE", year_expr, last_month_expr, exp.Literal.number(1)
 205        )
 206
 207        # Last day of the last month of the quarter
 208        last_day_expr = exp.func("LAST_DAY", first_day_last_month_expr)
 209        return self.sql(last_day_expr)
 210
 211    self.unsupported(f"Unsupported date part '{unit}' in LAST_DAY function")
 212    return self.function_fallback_sql(expression)
 213
 214
 215def _is_nanosecond_unit(unit: exp.Expr | None) -> bool:
 216    return isinstance(unit, (exp.Var, exp.Literal)) and unit.name.upper() == "NANOSECOND"
 217
 218
 219def _handle_nanosecond_diff(
 220    self: DuckDBGenerator,
 221    end_time: exp.Expr,
 222    start_time: exp.Expr,
 223) -> str:
 224    """Generate NANOSECOND diff using EPOCH_NS since DATE_DIFF doesn't support it."""
 225    end_ns = exp.cast(end_time, exp.DType.TIMESTAMP_NS)
 226    start_ns = exp.cast(start_time, exp.DType.TIMESTAMP_NS)
 227
 228    # Build expression tree: EPOCH_NS(end) - EPOCH_NS(start)
 229    return self.sql(
 230        exp.Sub(this=exp.func("EPOCH_NS", end_ns), expression=exp.func("EPOCH_NS", start_ns))
 231    )
 232
 233
 234def _to_boolean_sql(self: DuckDBGenerator, expression: exp.ToBoolean) -> str:
 235    """
 236    Transpile TO_BOOLEAN and TRY_TO_BOOLEAN functions from Snowflake to DuckDB equivalent.
 237
 238    DuckDB's CAST to BOOLEAN supports most of Snowflake's TO_BOOLEAN strings except 'on'/'off'.
 239    We need to handle the 'on'/'off' cases explicitly.
 240
 241    For TO_BOOLEAN (safe=False): NaN and INF values cause errors. We use DuckDB's native ERROR()
 242    function to replicate this behavior with a clear error message.
 243
 244    For TRY_TO_BOOLEAN (safe=True): Use DuckDB's TRY_CAST for conversion, which returns NULL
 245    for invalid inputs instead of throwing errors.
 246    """
 247    arg = expression.this
 248    is_safe = expression.args.get("safe", False)
 249
 250    base_case_expr = (
 251        exp.case()
 252        .when(
 253            # Handle 'on' -> TRUE (case insensitive)
 254            exp.Upper(this=exp.cast(arg, exp.DType.VARCHAR)).eq(exp.Literal.string("ON")),
 255            exp.true(),
 256        )
 257        .when(
 258            # Handle 'off' -> FALSE (case insensitive)
 259            exp.Upper(this=exp.cast(arg, exp.DType.VARCHAR)).eq(exp.Literal.string("OFF")),
 260            exp.false(),
 261        )
 262    )
 263
 264    if is_safe:
 265        # TRY_TO_BOOLEAN: handle 'on'/'off' and use TRY_CAST for everything else
 266        case_expr = base_case_expr.else_(exp.func("TRY_CAST", arg, exp.DType.BOOLEAN.into_expr()))
 267    else:
 268        # TO_BOOLEAN: handle NaN/INF errors, 'on'/'off', and use regular CAST
 269        cast_to_real = exp.func("TRY_CAST", arg, exp.DType.FLOAT.into_expr())
 270
 271        # Check for NaN and INF values
 272        nan_inf_check = exp.Or(
 273            this=exp.func("ISNAN", cast_to_real), expression=exp.func("ISINF", cast_to_real)
 274        )
 275
 276        case_expr = base_case_expr.when(
 277            nan_inf_check,
 278            exp.func(
 279                "ERROR",
 280                exp.Literal.string("TO_BOOLEAN: Non-numeric values NaN and INF are not supported"),
 281            ),
 282        ).else_(exp.cast(arg, exp.DType.BOOLEAN))
 283
 284    return self.sql(case_expr)
 285
 286
 287# BigQuery -> DuckDB conversion for the DATE function
 288def _date_sql(self: DuckDBGenerator, expression: exp.Date) -> str:
 289    this = expression.this
 290    zone = self.sql(expression, "zone")
 291
 292    if zone:
 293        # BigQuery considers "this" at UTC, converts it to the specified
 294        # time zone and then keeps only the DATE part
 295        # To micmic that, we:
 296        #   (1) Cast to TIMESTAMP to remove DuckDB's local tz
 297        #   (2) Apply consecutive AtTimeZone calls for UTC -> zone conversion
 298        this = exp.cast(this, exp.DType.TIMESTAMP)
 299        at_utc = exp.AtTimeZone(this=this, zone=exp.Literal.string("UTC"))
 300        this = exp.AtTimeZone(this=at_utc, zone=zone)
 301
 302    return self.sql(exp.cast(expression=this, to=exp.DType.DATE))
 303
 304
 305# BigQuery -> DuckDB conversion for the TIME_DIFF function
 306def _timediff_sql(self: DuckDBGenerator, expression: exp.TimeDiff) -> str:
 307    unit = expression.unit
 308
 309    if _is_nanosecond_unit(unit):
 310        return _handle_nanosecond_diff(self, expression.expression, expression.this)
 311
 312    this = exp.cast(expression.this, exp.DType.TIME)
 313    expr = exp.cast(expression.expression, exp.DType.TIME)
 314
 315    # Although the 2 dialects share similar signatures, BQ seems to inverse
 316    # the sign of the result so the start/end time operands are flipped
 317    return self.func("DATE_DIFF", unit_to_str(expression), expr, this)
 318
 319
 320def _date_delta_to_binary_interval_op(
 321    cast: bool = True,
 322) -> t.Callable[[DuckDBGenerator, DATETIME_DELTA], str]:
 323    """
 324    DuckDB override to handle:
 325    1. NANOSECOND operations (DuckDB doesn't support INTERVAL ... NANOSECOND)
 326    2. Float/decimal interval values (DuckDB INTERVAL requires integers)
 327    """
 328    base_impl = date_delta_to_binary_interval_op(cast=cast)
 329
 330    def _duckdb_date_delta_sql(self: DuckDBGenerator, expression: DATETIME_DELTA) -> str:
 331        unit = expression.unit
 332        interval_value = expression.expression
 333
 334        # Handle NANOSECOND unit (DuckDB doesn't support INTERVAL ... NANOSECOND)
 335        if _is_nanosecond_unit(unit):
 336            if isinstance(interval_value, exp.Interval):
 337                interval_value = interval_value.this
 338
 339            timestamp_ns = exp.cast(expression.this, exp.DType.TIMESTAMP_NS)
 340
 341            return self.sql(
 342                exp.func(
 343                    "MAKE_TIMESTAMP_NS",
 344                    exp.Add(this=exp.func("EPOCH_NS", timestamp_ns), expression=interval_value),
 345                )
 346            )
 347
 348        # Handle float/decimal interval values as duckDB INTERVAL requires integer expressions
 349        if not interval_value or isinstance(interval_value, exp.Interval):
 350            return base_impl(self, expression)
 351
 352        if interval_value.is_type(*exp.DataType.REAL_TYPES):
 353            expression.set("expression", exp.cast(exp.func("ROUND", interval_value), "INT"))
 354
 355        return base_impl(self, expression)
 356
 357    return _duckdb_date_delta_sql
 358
 359
 360def _array_insert_sql(self: DuckDBGenerator, expression: exp.ArrayInsert) -> str:
 361    """
 362    Transpile ARRAY_INSERT to DuckDB using LIST_CONCAT and slicing.
 363
 364    Handles:
 365    - 0-based and 1-based indexing (normalizes to 0-based for calculations)
 366    - Negative position conversion (requires array length)
 367    - NULL propagation (source dialects return NULL, DuckDB creates single-element array)
 368    - Assumes position is within bounds per user constraint
 369
 370    Note: All dialects that support ARRAY_INSERT (Snowflake, Spark, Databricks) have
 371    ARRAY_FUNCS_PROPAGATES_NULLS=True, so we always assume source propagates NULLs.
 372
 373    Args:
 374        expression: The ArrayInsert expression to transpile.
 375
 376    Returns:
 377        SQL string implementing ARRAY_INSERT behavior.
 378    """
 379    this = expression.this
 380    position = expression.args.get("position")
 381    element = expression.expression
 382    element_array = exp.Array(expressions=[element])
 383    index_offset = expression.args.get("offset", 0)
 384
 385    if not position or not position.is_int:
 386        self.unsupported("ARRAY_INSERT can only be transpiled with a literal position")
 387        return self.func("ARRAY_INSERT", this, position, element)
 388
 389    pos_value = position.to_py()
 390
 391    # Normalize one-based indexing to zero-based for slice calculations
 392    # Spark (1-based) -> Snowflake (0-based):
 393    #   Positive: pos=1 -> pos=0 (subtract 1)
 394    #   Negative: pos=-2 -> pos=-1 (add 1)
 395    # Example: Spark array_insert([a,b,c], -2, d) -> [a,b,d,c] is same as Snowflake pos=-1
 396    if pos_value > 0:
 397        pos_value = pos_value - index_offset
 398    elif pos_value < 0:
 399        pos_value = pos_value + index_offset
 400
 401    # Build the appropriate list_concat expression based on position
 402    if pos_value == 0:
 403        # insert at beginning
 404        concat_exprs = [element_array, this]
 405    elif pos_value > 0:
 406        # Positive position: LIST_CONCAT(arr[1:pos], [elem], arr[pos+1:])
 407        # 0-based -> DuckDB 1-based slicing
 408
 409        # left slice: arr[1:pos]
 410        slice_start = exp.Bracket(
 411            this=this,
 412            expressions=[
 413                exp.Slice(this=exp.Literal.number(1), expression=exp.Literal.number(pos_value))
 414            ],
 415        )
 416
 417        # right slice: arr[pos+1:]
 418        slice_end = exp.Bracket(
 419            this=this, expressions=[exp.Slice(this=exp.Literal.number(pos_value + 1))]
 420        )
 421
 422        concat_exprs = [slice_start, element_array, slice_end]
 423    else:
 424        # Negative position: arr[1:LEN(arr)+pos], [elem], arr[LEN(arr)+pos+1:]
 425        # pos=-1 means insert before last element
 426        arr_len = exp.Length(this=this)
 427
 428        # Calculate slice position: LEN(arr) + pos (e.g., LEN(arr) + (-1) = LEN(arr) - 1)
 429        slice_end_pos = arr_len + exp.Literal.number(pos_value)
 430        slice_start_pos = slice_end_pos + exp.Literal.number(1)
 431
 432        # left slice: arr[1:LEN(arr)+pos]
 433        slice_start = exp.Bracket(
 434            this=this,
 435            expressions=[exp.Slice(this=exp.Literal.number(1), expression=slice_end_pos)],
 436        )
 437
 438        # right slice: arr[LEN(arr)+pos+1:]
 439        slice_end = exp.Bracket(this=this, expressions=[exp.Slice(this=slice_start_pos)])
 440
 441        concat_exprs = [slice_start, element_array, slice_end]
 442
 443    # All dialects that support ARRAY_INSERT propagate NULLs (Snowflake/Spark/Databricks)
 444    # Wrap in CASE WHEN array IS NULL THEN NULL ELSE func_expr END
 445    return self.sql(
 446        exp.If(
 447            this=exp.Is(this=this, expression=exp.Null()),
 448            true=exp.Null(),
 449            false=self.func("LIST_CONCAT", *concat_exprs),
 450        )
 451    )
 452
 453
 454def _array_remove_at_sql(self: DuckDBGenerator, expression: exp.ArrayRemoveAt) -> str:
 455    """
 456    Transpile ARRAY_REMOVE_AT to DuckDB using LIST_CONCAT and slicing.
 457
 458    Handles:
 459    - Positive positions (0-based indexing)
 460    - Negative positions (from end of array)
 461    - NULL propagation (Snowflake returns NULL for NULL array, DuckDB doesn't auto-propagate)
 462    - Only supports literal integer positions (non-literals remain untranspiled)
 463
 464    Transpilation patterns:
 465    - pos=0 (first): arr[2:]
 466    - pos>0 (middle): LIST_CONCAT(arr[1:p], arr[p+2:])
 467    - pos=-1 (last): arr[1:LEN(arr)-1]
 468    - pos<-1: LIST_CONCAT(arr[1:LEN(arr)+p], arr[LEN(arr)+p+2:])
 469
 470    All wrapped in: CASE WHEN arr IS NULL THEN NULL ELSE ... END
 471
 472    Args:
 473        expression: The ArrayRemoveAt expression to transpile.
 474
 475    Returns:
 476        SQL string implementing ARRAY_REMOVE_AT behavior.
 477    """
 478    this = expression.this
 479    position = expression.args.get("position")
 480
 481    if not position or not position.is_int:
 482        self.unsupported("ARRAY_REMOVE_AT can only be transpiled with a literal position")
 483        return self.func("ARRAY_REMOVE_AT", this, position)
 484
 485    pos_value = position.to_py()
 486
 487    # Build the appropriate expression based on position
 488    if pos_value == 0:
 489        # Remove first element: arr[2:]
 490        result_expr: exp.Expr | str = exp.Bracket(
 491            this=this,
 492            expressions=[exp.Slice(this=exp.Literal.number(2))],
 493        )
 494    elif pos_value > 0:
 495        # Remove at positive position: LIST_CONCAT(arr[1:pos], arr[pos+2:])
 496        # DuckDB uses 1-based slicing
 497        left_slice = exp.Bracket(
 498            this=this,
 499            expressions=[
 500                exp.Slice(this=exp.Literal.number(1), expression=exp.Literal.number(pos_value))
 501            ],
 502        )
 503        right_slice = exp.Bracket(
 504            this=this,
 505            expressions=[exp.Slice(this=exp.Literal.number(pos_value + 2))],
 506        )
 507        result_expr = self.func("LIST_CONCAT", left_slice, right_slice)
 508    elif pos_value == -1:
 509        # Remove last element: arr[1:LEN(arr)-1]
 510        # Optimization: simpler than general negative case
 511        arr_len = exp.Length(this=this)
 512        slice_end = arr_len + exp.Literal.number(-1)
 513        result_expr = exp.Bracket(
 514            this=this,
 515            expressions=[exp.Slice(this=exp.Literal.number(1), expression=slice_end)],
 516        )
 517    else:
 518        # Remove at negative position: LIST_CONCAT(arr[1:LEN(arr)+pos], arr[LEN(arr)+pos+2:])
 519        arr_len = exp.Length(this=this)
 520        slice_end_pos = arr_len + exp.Literal.number(pos_value)
 521        slice_start_pos = slice_end_pos + exp.Literal.number(2)
 522
 523        left_slice = exp.Bracket(
 524            this=this,
 525            expressions=[exp.Slice(this=exp.Literal.number(1), expression=slice_end_pos)],
 526        )
 527        right_slice = exp.Bracket(
 528            this=this,
 529            expressions=[exp.Slice(this=slice_start_pos)],
 530        )
 531        result_expr = self.func("LIST_CONCAT", left_slice, right_slice)
 532
 533    # Snowflake ARRAY_FUNCS_PROPAGATES_NULLS=True, so wrap in NULL check
 534    # CASE WHEN array IS NULL THEN NULL ELSE result_expr END
 535    return self.sql(
 536        exp.If(
 537            this=exp.Is(this=this, expression=exp.Null()),
 538            true=exp.Null(),
 539            false=result_expr,
 540        )
 541    )
 542
 543
 544@unsupported_args(("expression", "DuckDB's ARRAY_SORT does not support a comparator."))
 545def _array_sort_sql(self: DuckDBGenerator, expression: exp.ArraySort) -> str:
 546    return self.func("ARRAY_SORT", expression.this)
 547
 548
 549def _array_contains_sql(self: DuckDBGenerator, expression: exp.ArrayContains) -> str:
 550    this = expression.this
 551    expr = expression.expression
 552
 553    func = self.func("ARRAY_CONTAINS", this, expr)
 554
 555    if expression.args.get("check_null"):
 556        check_null_in_array = exp.Nullif(
 557            this=exp.NEQ(this=exp.ArraySize(this=this), expression=exp.func("LIST_COUNT", this)),
 558            expression=exp.false(),
 559        )
 560        return self.sql(exp.If(this=expr.is_(exp.Null()), true=check_null_in_array, false=func))
 561
 562    return func
 563
 564
 565def _array_overlaps_sql(self: DuckDBGenerator, expression: exp.ArrayOverlaps) -> str:
 566    """
 567    Translates Snowflake's NULL-safe ARRAYS_OVERLAP to DuckDB.
 568
 569    DuckDB's native && operator is not NULL-safe: [1,NULL,3] && [NULL,4,5] returns FALSE.
 570    Snowflake returns TRUE when both arrays contain NULL (NULLs are treated as known values).
 571
 572    Generated SQL: (arr1 && arr2) OR (ARRAY_LENGTH(arr1) <> LIST_COUNT(arr1) AND ARRAY_LENGTH(arr2) <> LIST_COUNT(arr2))
 573
 574    ARRAY_LENGTH counts all elements (including NULLs); LIST_COUNT counts only non-NULLs.
 575    When they differ, the array contains at least one NULL, matching Snowflake's NULL-safe semantics.
 576    """
 577    if not expression.args.get("null_safe"):
 578        return self.binary(expression, "&&")
 579
 580    arr1 = expression.this
 581    arr2 = expression.expression
 582
 583    check_nulls = exp.and_(
 584        exp.NEQ(
 585            this=exp.ArraySize(this=arr1.copy()),
 586            expression=exp.func("LIST_COUNT", arr1.copy()),
 587        ),
 588        exp.NEQ(
 589            this=exp.ArraySize(this=arr2.copy()),
 590            expression=exp.func("LIST_COUNT", arr2.copy()),
 591        ),
 592        copy=False,
 593    )
 594
 595    overlap = exp.ArrayOverlaps(this=arr1.copy(), expression=arr2.copy())
 596
 597    return self.sql(
 598        exp.or_(
 599            exp.paren(overlap, copy=False),
 600            exp.paren(check_nulls, copy=False),
 601            copy=False,
 602            wrap=False,
 603        )
 604    )
 605
 606
 607def _struct_sql(self: DuckDBGenerator, expression: exp.Struct) -> str:
 608    ancestor_cast = expression.find_ancestor(exp.Cast, exp.Select)
 609    ancestor_cast = None if isinstance(ancestor_cast, exp.Select) else ancestor_cast
 610
 611    # Empty struct cast works with MAP() since DuckDB can't parse {}
 612    if not expression.expressions:
 613        if isinstance(ancestor_cast, exp.Cast) and ancestor_cast.to.is_type(exp.DType.MAP):
 614            return "MAP()"
 615
 616    args: list[str] = []
 617
 618    # BigQuery allows inline construction such as "STRUCT<a STRING, b INTEGER>('str', 1)" which is
 619    # canonicalized to "ROW('str', 1) AS STRUCT(a TEXT, b INT)" in DuckDB
 620    # The transformation to ROW will take place if:
 621    #  1. The STRUCT itself does not have proper fields (key := value) as a "proper" STRUCT would
 622    #  2. A cast to STRUCT / ARRAY of STRUCTs is found
 623    is_bq_inline_struct = (
 624        (expression.find(exp.PropertyEQ) is None)
 625        and ancestor_cast
 626        and any(
 627            casted_type.is_type(exp.DType.STRUCT)
 628            for casted_type in ancestor_cast.find_all(exp.DataType)
 629        )
 630    )
 631
 632    for i, expr in enumerate(expression.expressions):
 633        is_property_eq = isinstance(expr, exp.PropertyEQ)
 634        this = expr.this
 635        value = expr.expression if is_property_eq else expr
 636
 637        if is_bq_inline_struct:
 638            args.append(self.sql(value))
 639        else:
 640            if isinstance(this, exp.Identifier):
 641                key = self.sql(exp.Literal.string(expr.name))
 642            elif is_property_eq:
 643                key = self.sql(this)
 644            else:
 645                key = self.sql(exp.Literal.string(f"_{i}"))
 646
 647            args.append(f"{key}: {self.sql(value)}")
 648
 649    csv_args = ", ".join(args)
 650
 651    return f"ROW({csv_args})" if is_bq_inline_struct else f"{{{csv_args}}}"
 652
 653
 654def _datatype_sql(self: DuckDBGenerator, expression: exp.DataType) -> str:
 655    if expression.is_type("array"):
 656        return f"{self.expressions(expression, flat=True)}[{self.expressions(expression, key='values', flat=True)}]"
 657
 658    # Modifiers are not supported for TIME, [TIME | TIMESTAMP] WITH TIME ZONE
 659    if expression.is_type(exp.DType.TIME, exp.DType.TIMETZ, exp.DType.TIMESTAMPTZ):
 660        return expression.this.value
 661
 662    return self.datatype_sql(expression)
 663
 664
 665def _json_format_sql(self: DuckDBGenerator, expression: exp.JSONFormat) -> str:
 666    sql = self.func("TO_JSON", expression.this, expression.args.get("options"))
 667    return f"CAST({sql} AS TEXT)"
 668
 669
 670def _build_seq_expression(base: exp.Expr, byte_width: int, signed: bool) -> exp.Expr:
 671    """Build a SEQ expression with the given base, byte width, and signedness."""
 672    bits = byte_width * 8
 673    max_val = exp.Literal.number(2**bits)
 674
 675    if signed:
 676        half = exp.Literal.number(2 ** (bits - 1))
 677        return exp.replace_placeholders(_SEQ_SIGNED.copy(), base=base, max_val=max_val, half=half)
 678    return exp.replace_placeholders(_SEQ_UNSIGNED.copy(), base=base, max_val=max_val)
 679
 680
 681def _seq_to_range_in_generator(expression: exp.Expr) -> exp.Expr:
 682    """
 683    Transform SEQ functions to `range` column references when inside a GENERATOR context.
 684
 685    When GENERATOR(ROWCOUNT => N) becomes RANGE(N) in DuckDB, it produces a column
 686    named `range` with values 0, 1, ..., N-1. SEQ functions produce the same sequence,
 687    so we replace them with `range % max_val` to avoid nested window function issues.
 688    """
 689    if not isinstance(expression, exp.Select):
 690        return expression
 691
 692    from_ = expression.args.get("from_")
 693    if not (
 694        from_
 695        and isinstance(from_.this, exp.TableFromRows)
 696        and isinstance(from_.this.this, exp.Generator)
 697    ):
 698        return expression
 699
 700    def replace_seq(node: exp.Expr) -> exp.Expr:
 701        if isinstance(node, (exp.Seq1, exp.Seq2, exp.Seq4, exp.Seq8)):
 702            byte_width = _SEQ_BYTE_WIDTH[type(node)]
 703            return _build_seq_expression(exp.column("range"), byte_width, signed=node.name == "1")
 704        return node
 705
 706    return expression.transform(replace_seq, copy=False)
 707
 708
 709def connect_by_to_recursive_cte(expression: exp.Expr) -> exp.Expr:
 710    # Rewrites START WITH ... CONNECT BY PRIOR into WITH RECURSIVE
 711    # Falls through unchanged if there are no PRIORs.
 712    if not isinstance(expression, exp.Select) or not expression.args.get("connect"):
 713        return expression
 714
 715    connect = expression.args["connect"]
 716    connect_pred = connect.args["connect"]
 717
 718    priors = list(connect_pred.find_all(exp.Prior))
 719    if not priors:
 720        return expression
 721
 722    from_ = expression.args.get("from_")
 723    if not from_ or expression.args.get("joins"):
 724        return expression
 725
 726    source_table = from_.this
 727    base_select_exprs = expression.expressions
 728    base_where = expression.args.get("where")
 729    base_with = expression.args.get("with_")
 730
 731    # LEVEL is a Snowflake pseudo-column: it's always computed as a depth counter in the CTE.
 732    has_level = any(
 733        isinstance(col, exp.Column) and col.name.upper() == "LEVEL"
 734        for e in base_select_exprs
 735        for col in e.find_all(exp.Column)
 736    )
 737    has_star = expression.is_star
 738
 739    # CONNECT_BY_ROOT col yields the value of `col` from the START WITH row that begins each
 740    # branch. Each one is threaded through the CTE as an extra column: the anchor binds it to the
 741    # row's own value, the recursive arm forwards the parent's value unchanged.
 742    root_col_names: list[str] = []
 743    anchor_root_cols: list[exp.Expr] = []
 744    inner_root_cols: list[exp.Expr] = []
 745    roots = [root for e in base_select_exprs for root in e.find_all(exp.ConnectByRoot)]
 746
 747    for i, root in enumerate(roots):
 748        name = f"_connect_by_root_{i}"
 749        root_col_names.append(name)
 750        anchor_root_cols.append(exp.alias_(root.this, name))
 751        inner_root_cols.append(exp.alias_(exp.column(name, "_parent_row"), name))
 752        root.replace(exp.column(name))
 753
 754    # Build the join condition from the full CONNECT BY predicate:
 755    # PRIOR(col) → _parent_row.col, unqualified cols → _child_row.col.
 756    def _qualify_connect_pred(node: exp.Expression) -> exp.Expression:
 757        for col in find_all_in_scope(node, exp.Column):
 758            col.set(
 759                "table",
 760                exp.to_identifier(
 761                    "_parent_row" if isinstance(col.parent, exp.Prior) else "_child_row"
 762                ),
 763            )
 764        for prior in find_all_in_scope(node, exp.Prior):
 765            prior.replace(prior.this)
 766        return node
 767
 768    # Avoid colliding with any CTE names already on the query.
 769    cte_name = find_new_name(
 770        {cte.alias for cte in (base_with.expressions if base_with else [])}, "_rootcte"
 771    )
 772
 773    # Anchor: project all source columns + seed LEVEL at 1 + bind each root column to its own value.
 774    anchor = exp.select(
 775        exp.Star(), exp.alias_(exp.Literal.number(1), "level"), *anchor_root_cols
 776    ).from_(source_table)
 777    if connect.args.get("start"):
 778        anchor = anchor.where(connect.args["start"])
 779
 780    # Recursive arm: carry all child columns + increment level + forward each root value.
 781    # SELECT * in both arms means WHERE/PRIOR columns are always available without explicit tracking.
 782    inner_query = (
 783        exp.select(
 784            exp.Column(this=exp.Star(), table=exp.to_identifier("_child_row")),
 785            exp.alias_(exp.column("level", "_parent_row") + 1, "level"),
 786            *inner_root_cols,
 787        )
 788        .from_(source_table.as_("_child_row"))
 789        .join(exp.to_table(cte_name).as_("_parent_row"), on=_qualify_connect_pred(connect_pred))
 790    )
 791
 792    # Outer SELECT re-projects from the CTE. Synthetic level/root columns are excluded from any
 793    # star expansion (level only when not referenced) but kept where explicitly projected.
 794    if has_star:
 795        except_cols = [] if has_level else [exp.column("level")]
 796        except_cols.extend(exp.column(name) for name in root_col_names)
 797        star = exp.Star(except_=except_cols) if except_cols else exp.Star()
 798        outer_select_exprs: list[exp.Expr] = [
 799            star,
 800            *(e for e in base_select_exprs if not e.is_star),
 801        ]
 802    else:
 803        outer_select_exprs = base_select_exprs
 804    outer_query = exp.select(*outer_select_exprs).from_(cte_name)
 805    if base_where:
 806        outer_query = outer_query.where(base_where.this)
 807
 808    # Attach the CTE, marking the WITH clause recursive.
 809    if base_with:
 810        outer_query.set("with_", base_with)
 811    outer_query = outer_query.with_(
 812        cte_name, as_=anchor.union(inner_query, distinct=False), recursive=True, copy=False
 813    )
 814
 815    for arg, val in expression.args.items():
 816        if val and arg not in _CONNECT_BY_ARGS_TO_SKIP:
 817            outer_query.set(arg, val)
 818
 819    # Strip stale source table qualifiers in one pass; CTEs are child scopes so
 820    # find_all_in_scope stays within the outer query only.
 821    for col in find_all_in_scope(outer_query, exp.Column):
 822        col.set("table", None)
 823
 824    return outer_query
 825
 826
 827def _seq_sql(self: DuckDBGenerator, expression: exp.Func, byte_width: int) -> str:
 828    """
 829    Transpile Snowflake SEQ1/SEQ2/SEQ4/SEQ8 to DuckDB.
 830
 831    Generates monotonically increasing integers starting from 0.
 832    The signed parameter (0 or 1) affects wrap-around behavior:
 833    - Unsigned (0): wraps at 2^(bits) - 1
 834    - Signed (1): wraps at 2^(bits-1) - 1, then goes negative
 835    """
 836    # Warn if SEQ is in a restricted context (Select stops search at current scope)
 837    ancestor = expression.find_ancestor(*_SEQ_RESTRICTED)
 838    if ancestor and (
 839        (not isinstance(ancestor, (exp.Order, exp.Select)))
 840        or (isinstance(ancestor, exp.Order) and isinstance(ancestor.parent, exp.Window))
 841    ):
 842        self.unsupported("SEQ in restricted context is not supported - use CTE or subquery")
 843
 844    result = _build_seq_expression(_SEQ_BASE.copy(), byte_width, signed=expression.name == "1")
 845    return self.sql(result)
 846
 847
 848def _unix_to_time_sql(self: DuckDBGenerator, expression: exp.UnixToTime) -> str:
 849    scale = expression.args.get("scale")
 850    timestamp = expression.this
 851    target_type = expression.args.get("target_type")
 852
 853    # Check if we need NTZ (naive timestamp in UTC)
 854    is_ntz = target_type and target_type.this in (
 855        exp.DType.TIMESTAMP,
 856        exp.DType.TIMESTAMPNTZ,
 857    )
 858
 859    if scale == exp.UnixToTime.MILLIS:
 860        # EPOCH_MS already returns TIMESTAMP (naive, UTC)
 861        return self.func("EPOCH_MS", timestamp)
 862    if scale == exp.UnixToTime.MICROS:
 863        # MAKE_TIMESTAMP already returns TIMESTAMP (naive, UTC)
 864        return self.func("MAKE_TIMESTAMP", timestamp)
 865
 866    # Other scales: divide and use TO_TIMESTAMP
 867    if scale not in (None, exp.UnixToTime.SECONDS):
 868        timestamp = exp.Div(this=timestamp, expression=exp.func("POW", 10, scale))
 869
 870    to_timestamp: exp.Expr = exp.Anonymous(this="TO_TIMESTAMP", expressions=[timestamp])
 871
 872    if is_ntz:
 873        to_timestamp = exp.AtTimeZone(this=to_timestamp, zone=exp.Literal.string("UTC"))
 874
 875    return self.sql(to_timestamp)
 876
 877
 878WRAPPED_JSON_EXTRACT_EXPRESSIONS = (exp.Binary, exp.Bracket, exp.In, exp.Not)
 879
 880
 881def _arrow_json_extract_sql(self: DuckDBGenerator, expression: JSON_EXTRACT_TYPE) -> str:
 882    arrow_sql = arrow_json_extract_sql(self, expression)
 883    if not expression.same_parent and isinstance(
 884        expression.parent, WRAPPED_JSON_EXTRACT_EXPRESSIONS
 885    ):
 886        arrow_sql = self.wrap(arrow_sql)
 887    return arrow_sql
 888
 889
 890def _implicit_datetime_cast(
 891    arg: exp.Expr | None, type: exp.DType = exp.DType.DATE
 892) -> exp.Expr | None:
 893    if isinstance(arg, exp.Literal) and arg.is_string:
 894        ts = arg.name
 895        if type == exp.DType.DATE and ":" in ts:
 896            type = exp.DType.TIMESTAMPTZ if TIMEZONE_PATTERN.search(ts) else exp.DType.TIMESTAMP
 897
 898        arg = exp.cast(arg, type)
 899
 900    return arg
 901
 902
 903def _week_trunc_start_dow(unit: exp.Expr | None) -> int | None:
 904    # DuckDB's weeks are ISO 8601, so ISOWEEK maps to its plain WEEK unit
 905    if isinstance(unit, exp.Literal) and unit.name.upper() == "ISOWEEK":
 906        return 1
 907    return week_unit_to_dow(unit)
 908
 909
 910def _build_week_trunc_expression(
 911    date_expr: exp.Expr,
 912    start_dow: int,
 913    preserve_start_day: bool = False,
 914    cast_to_date: bool = True,
 915) -> exp.Expr:
 916    """
 917    Build DATE_TRUNC expression for week boundaries with custom start day.
 918
 919    DuckDB's DATE_TRUNC('WEEK', ...) always returns Monday. To align to a different
 920    start day, we shift the date before truncating.
 921
 922    Args:
 923        date_expr: The date expression to truncate.
 924        start_dow: ISO 8601 day-of-week number (Monday=1, ..., Sunday=7).
 925        preserve_start_day: If True, reverse the shift after truncating so the result lands on the
 926            correct week start day. Needed for DATE_TRUNC (absolute result matters) but
 927            not for DATE_DIFF (only relative alignment matters).
 928        cast_to_date: If True, cast the shifted result back to DATE; set to False for
 929            timestamp-valued inputs, where the result must remain a timestamp.
 930
 931    Shift formula: Sunday (7) gets +1, others get (1 - start_dow).
 932    """
 933    shift_days = 1 if start_dow == 7 else 1 - start_dow
 934    truncated = exp.func("DATE_TRUNC", unit=exp.var("WEEK"), this=date_expr)
 935
 936    if shift_days == 0:
 937        return truncated
 938
 939    shift = exp.Interval(this=exp.Literal.string(str(shift_days)), unit=exp.var("DAY"))
 940    shifted_date = exp.DateAdd(this=date_expr, expression=shift)
 941    truncated.set("this", shifted_date)
 942
 943    if preserve_start_day:
 944        interval = exp.Interval(this=exp.Literal.string(str(-shift_days)), unit=exp.var("DAY"))
 945        shifted_back: exp.Expr = exp.DateAdd(this=truncated, expression=interval)
 946        if cast_to_date:
 947            return exp.cast(shifted_back, to=exp.DType.DATE, copy=False)
 948        return shifted_back
 949
 950    return truncated
 951
 952
 953def _date_diff_sql(self: DuckDBGenerator, expression: exp.DateDiff | exp.DatetimeDiff) -> str:
 954    unit = expression.unit
 955
 956    if _is_nanosecond_unit(unit):
 957        return _handle_nanosecond_diff(self, expression.this, expression.expression)
 958
 959    this = _implicit_datetime_cast(expression.this)
 960    expr = _implicit_datetime_cast(expression.expression)
 961
 962    # DuckDB's WEEK diff does not respect Monday crossing (week boundaries), it checks (end_day - start_day) / 7:
 963    #  SELECT DATE_DIFF('WEEK', CAST('2024-12-13' AS DATE), CAST('2024-12-17' AS DATE)) --> 0 (Monday crossed)
 964    #  SELECT DATE_DIFF('WEEK', CAST('2024-12-13' AS DATE), CAST('2024-12-20' AS DATE)) --> 1 (7 days difference)
 965    # Whereas for other units such as MONTH it does respect month boundaries:
 966    #  SELECT DATE_DIFF('MONTH', CAST('2024-11-30' AS DATE), CAST('2024-12-01' AS DATE)) --> 1 (Month crossed)
 967    date_part_boundary = expression.args.get("date_part_boundary")
 968
 969    # Extract week start day; returns None if day is dynamic (column/placeholder)
 970    week_start = week_unit_to_dow(unit)
 971    if date_part_boundary and week_start and this and expr:
 972        expression.set("unit", exp.Literal.string("WEEK"))
 973
 974        # Truncate both dates to week boundaries to respect input dialect semantics
 975        this = _build_week_trunc_expression(this, week_start)
 976        expr = _build_week_trunc_expression(expr, week_start)
 977
 978    return self.func("DATE_DIFF", unit_to_str(expression), expr, this)
 979
 980
 981def _generate_datetime_array_sql(
 982    self: DuckDBGenerator, expression: exp.GenerateDateArray | exp.GenerateTimestampArray
 983) -> str:
 984    is_generate_date_array = isinstance(expression, exp.GenerateDateArray)
 985
 986    type = exp.DType.DATE if is_generate_date_array else exp.DType.TIMESTAMP
 987    start = _implicit_datetime_cast(expression.args.get("start"), type=type)
 988    end = _implicit_datetime_cast(expression.args.get("end"), type=type)
 989
 990    # BQ's GENERATE_DATE_ARRAY & GENERATE_TIMESTAMP_ARRAY are transformed to DuckDB'S GENERATE_SERIES
 991    gen_series: exp.GenerateSeries | exp.Cast = exp.GenerateSeries(
 992        start=start, end=end, step=expression.args.get("step")
 993    )
 994
 995    if is_generate_date_array:
 996        # The GENERATE_SERIES result type is TIMESTAMP array, so to match BQ's semantics for
 997        # GENERATE_DATE_ARRAY we must cast it back to DATE array
 998        gen_series = exp.cast(gen_series, exp.DataType.from_str("ARRAY<DATE>"))
 999
1000    return self.sql(gen_series)
1001
1002
1003def _json_extract_value_array_sql(
1004    self: DuckDBGenerator, expression: exp.JSONValueArray | exp.JSONExtractArray
1005) -> str:
1006    json_extract = exp.JSONExtract(this=expression.this, expression=expression.expression)
1007    data_type = "ARRAY<STRING>" if isinstance(expression, exp.JSONValueArray) else "ARRAY<JSON>"
1008    return self.sql(exp.cast(json_extract, to=exp.DataType.from_str(data_type)))
1009
1010
1011def _cast_to_varchar(arg: exp.Expr | None) -> exp.Expr | None:
1012    if arg and arg.type and not arg.is_type(*exp.DataType.TEXT_TYPES, exp.DType.UNKNOWN):
1013        return exp.cast(arg, exp.DType.VARCHAR)
1014    return arg
1015
1016
1017def _cast_to_boolean(arg: exp.Expr | None) -> exp.Expr | None:
1018    if arg and not arg.is_type(exp.DType.BOOLEAN):
1019        return exp.cast(arg, exp.DType.BOOLEAN)
1020    return arg
1021
1022
1023def _is_binary(arg: exp.Expr) -> bool:
1024    return arg.is_type(
1025        exp.DType.BINARY,
1026        exp.DType.VARBINARY,
1027        exp.DType.BLOB,
1028    )
1029
1030
1031def _gen_with_cast_to_blob(self: DuckDBGenerator, expression: exp.Expr, result_sql: str) -> str:
1032    if _is_binary(expression):
1033        blob = exp.DataType.from_str("BLOB", dialect="duckdb")
1034        result_sql = self.sql(exp.Cast(this=result_sql, to=blob))
1035    return result_sql
1036
1037
1038def _cast_to_bit(arg: exp.Expr) -> exp.Expr:
1039    if not _is_binary(arg):
1040        return arg
1041
1042    if isinstance(arg, exp.HexString):
1043        arg = exp.Unhex(this=exp.Literal.string(arg.this))
1044
1045    return exp.cast(arg, exp.DType.BIT)
1046
1047
1048def _prepare_binary_bitwise_args(expression: exp.Binary) -> None:
1049    if _is_binary(expression.this):
1050        expression.set("this", _cast_to_bit(expression.this))
1051    if _is_binary(expression.expression):
1052        expression.set("expression", _cast_to_bit(expression.expression))
1053
1054
1055def _day_navigation_sql(self: DuckDBGenerator, expression: exp.NextDay | exp.PreviousDay) -> str:
1056    """
1057    Transpile Snowflake's NEXT_DAY / PREVIOUS_DAY to DuckDB using date arithmetic.
1058
1059    Returns the DATE of the next/previous occurrence of the specified weekday.
1060
1061    Formulas:
1062    - NEXT_DAY: (target_dow - current_dow + 6) % 7 + 1
1063    - PREVIOUS_DAY: (current_dow - target_dow + 6) % 7 + 1
1064
1065    Supports both literal and non-literal day names:
1066    - Literal: Direct lookup (e.g., 'Monday' -> 1)
1067    - Non-literal: CASE statement for runtime evaluation
1068
1069    Examples:
1070        NEXT_DAY('2024-01-01' (Monday), 'Monday')
1071          -> (1 - 1 + 6) % 7 + 1 = 6 % 7 + 1 = 7 days -> 2024-01-08
1072
1073        PREVIOUS_DAY('2024-01-15' (Monday), 'Friday')
1074          -> (1 - 5 + 6) % 7 + 1 = 2 % 7 + 1 = 3 days -> 2024-01-12
1075    """
1076    date_expr = expression.this
1077    day_name_expr = expression.expression
1078
1079    # Build ISODOW call for current day of week
1080    isodow_call = exp.func("ISODOW", date_expr)
1081
1082    # Determine target day of week
1083    if isinstance(day_name_expr, exp.Literal):
1084        # Literal day name: lookup target_dow directly
1085        day_name_str = day_name_expr.name.upper()
1086        matching_day = next(
1087            (day for day in WEEK_START_DAY_TO_DOW if day.startswith(day_name_str)), None
1088        )
1089        if matching_day:
1090            target_dow: exp.Expr = exp.Literal.number(WEEK_START_DAY_TO_DOW[matching_day])
1091        else:
1092            # Unrecognized day name, use fallback
1093            return self.function_fallback_sql(expression)
1094    else:
1095        # Non-literal day name: build CASE statement for runtime mapping
1096        upper_day_name = exp.Upper(this=day_name_expr)
1097        target_dow = exp.Case(
1098            ifs=[
1099                exp.If(
1100                    this=exp.func(
1101                        "STARTS_WITH", upper_day_name.copy(), exp.Literal.string(day[:2])
1102                    ),
1103                    true=exp.Literal.number(dow_num),
1104                )
1105                for day, dow_num in WEEK_START_DAY_TO_DOW.items()
1106            ]
1107        )
1108
1109    # Calculate days offset and apply interval based on direction
1110    if isinstance(expression, exp.NextDay):
1111        # NEXT_DAY: (target_dow - current_dow + 6) % 7 + 1
1112        days_offset = exp.paren(target_dow - isodow_call + 6, copy=False) % 7 + 1
1113        date_with_offset = date_expr + exp.Interval(this=days_offset, unit=exp.var("DAY"))
1114    else:  # exp.PreviousDay
1115        # PREVIOUS_DAY: (current_dow - target_dow + 6) % 7 + 1
1116        days_offset = exp.paren(isodow_call - target_dow + 6, copy=False) % 7 + 1
1117        date_with_offset = date_expr - exp.Interval(this=days_offset, unit=exp.var("DAY"))
1118
1119    # Build final: CAST(date_with_offset AS DATE)
1120    return self.sql(exp.cast(date_with_offset, exp.DType.DATE))
1121
1122
1123def _anyvalue_sql(self: DuckDBGenerator, expression: exp.AnyValue) -> str:
1124    # Transform ANY_VALUE(expr HAVING MAX/MIN having_expr) to ARG_MAX_NULL/ARG_MIN_NULL
1125    having = expression.this
1126    if isinstance(having, exp.HavingMax):
1127        func_name = "ARG_MAX_NULL" if having.args.get("max") else "ARG_MIN_NULL"
1128        return self.func(func_name, having.this, having.expression)
1129    return self.function_fallback_sql(expression)
1130
1131
1132def _bitwise_agg_sql(
1133    self: DuckDBGenerator,
1134    expression: exp.BitwiseOrAgg | exp.BitwiseAndAgg | exp.BitwiseXorAgg,
1135) -> str:
1136    """
1137    DuckDB's bitwise aggregate functions only accept integer types. For other types:
1138    - DECIMAL/STRING: Use CAST(arg AS INT) to convert directly, will round to nearest int
1139    - FLOAT/DOUBLE: Use ROUND(arg)::INT to round to nearest integer, required due to float precision loss
1140    """
1141    if isinstance(expression, exp.BitwiseOrAgg):
1142        func_name = "BIT_OR"
1143    elif isinstance(expression, exp.BitwiseAndAgg):
1144        func_name = "BIT_AND"
1145    else:  # exp.BitwiseXorAgg
1146        func_name = "BIT_XOR"
1147
1148    arg = expression.this
1149
1150    if not arg.type:
1151        from sqlglot.optimizer.annotate_types import annotate_types
1152
1153        arg = annotate_types(arg, dialect=self.dialect)
1154
1155    if arg.is_type(*exp.DataType.REAL_TYPES, *exp.DataType.TEXT_TYPES):
1156        if arg.is_type(*exp.DataType.FLOAT_TYPES):
1157            # float types need to be rounded first due to precision loss
1158            arg = exp.func("ROUND", arg)
1159
1160        arg = exp.cast(arg, exp.DType.INT)
1161
1162    return self.func(func_name, arg)
1163
1164
1165def _literal_sql_with_ws_chr(self: DuckDBGenerator, literal: str) -> str:
1166    # DuckDB does not support \uXXXX escapes, so we must use CHR() instead of replacing them directly
1167    if not any(ch in WS_CONTROL_CHARS_TO_DUCK for ch in literal):
1168        return self.sql(exp.Literal.string(literal))
1169
1170    sql_segments: list[str] = []
1171    for is_ws_control, group in groupby(literal, key=lambda ch: ch in WS_CONTROL_CHARS_TO_DUCK):
1172        if is_ws_control:
1173            for ch in group:
1174                duckdb_char_code = WS_CONTROL_CHARS_TO_DUCK[ch]
1175                sql_segments.append(self.func("CHR", exp.Literal.number(str(duckdb_char_code))))
1176        else:
1177            sql_segments.append(self.sql(exp.Literal.string("".join(group))))
1178
1179    sql = " || ".join(sql_segments)
1180    return sql if len(sql_segments) == 1 else f"({sql})"
1181
1182
1183def _escape_regex_metachars(
1184    self: DuckDBGenerator, delimiters: exp.Expr | None, delimiters_sql: str
1185) -> str:
1186    r"""
1187    Escapes regex metacharacters \ - ^ [ ] for use in character classes regex expressions.
1188
1189    Literal strings are escaped at transpile time, expressions handled with REPLACE() calls.
1190    """
1191    if not delimiters:
1192        return delimiters_sql
1193
1194    if delimiters.is_string:
1195        literal_value = delimiters.this
1196        escaped_literal = "".join(REGEX_ESCAPE_REPLACEMENTS.get(ch, ch) for ch in literal_value)
1197        return _literal_sql_with_ws_chr(self, escaped_literal)
1198
1199    escaped_sql = delimiters_sql
1200    for raw, escaped in REGEX_ESCAPE_REPLACEMENTS.items():
1201        escaped_sql = self.func(
1202            "REPLACE",
1203            escaped_sql,
1204            self.sql(exp.Literal.string(raw)),
1205            self.sql(exp.Literal.string(escaped)),
1206        )
1207
1208    return escaped_sql
1209
1210
1211def _build_capitalization_sql(
1212    self: DuckDBGenerator,
1213    value_to_split: str,
1214    delimiters_sql: str,
1215) -> str:
1216    # empty string delimiter --> treat value as one word, no need to split
1217    if delimiters_sql == "''":
1218        return f"UPPER(LEFT({value_to_split}, 1)) || LOWER(SUBSTRING({value_to_split}, 2))"
1219
1220    delim_regex_sql = f"CONCAT('[', {delimiters_sql}, ']')"
1221    split_regex_sql = f"CONCAT('([', {delimiters_sql}, ']+|[^', {delimiters_sql}, ']+)')"
1222
1223    # REGEXP_EXTRACT_ALL produces a list of string segments, alternating between delimiter and non-delimiter segments.
1224    # We do not know whether the first segment is a delimiter or not, so we check the first character of the string
1225    # with REGEXP_MATCHES. If the first char is a delimiter, we capitalize even list indexes, otherwise capitalize odd.
1226    return self.func(
1227        "ARRAY_TO_STRING",
1228        exp.case()
1229        .when(
1230            f"REGEXP_MATCHES(LEFT({value_to_split}, 1), {delim_regex_sql})",
1231            self.func(
1232                "LIST_TRANSFORM",
1233                self.func("REGEXP_EXTRACT_ALL", value_to_split, split_regex_sql),
1234                "(seg, idx) -> CASE WHEN idx % 2 = 0 THEN UPPER(LEFT(seg, 1)) || LOWER(SUBSTRING(seg, 2)) ELSE seg END",
1235            ),
1236        )
1237        .else_(
1238            self.func(
1239                "LIST_TRANSFORM",
1240                self.func("REGEXP_EXTRACT_ALL", value_to_split, split_regex_sql),
1241                "(seg, idx) -> CASE WHEN idx % 2 = 1 THEN UPPER(LEFT(seg, 1)) || LOWER(SUBSTRING(seg, 2)) ELSE seg END",
1242            ),
1243        ),
1244        "''",
1245    )
1246
1247
1248def _initcap_sql(self: DuckDBGenerator, expression: exp.Initcap) -> str:
1249    this_sql = self.sql(expression, "this")
1250    delimiters = expression.args.get("expression")
1251    if delimiters is None:
1252        # fallback for manually created exp.Initcap w/o delimiters arg
1253        delimiters = exp.Literal.string(self.dialect.INITCAP_DEFAULT_DELIMITER_CHARS)
1254    delimiters_sql = self.sql(delimiters)
1255
1256    escaped_delimiters_sql = _escape_regex_metachars(self, delimiters, delimiters_sql)
1257
1258    return _build_capitalization_sql(self, this_sql, escaped_delimiters_sql)
1259
1260
1261def _boolxor_agg_sql(self: DuckDBGenerator, expression: exp.BoolxorAgg) -> str:
1262    """
1263    Snowflake's `BOOLXOR_AGG(col)` returns TRUE if exactly one input in `col` is TRUE, FALSE otherwise;
1264    Since DuckDB does not have a mapping function, we mimic the behavior by generating `COUNT_IF(col) = 1`.
1265
1266    DuckDB's COUNT_IF strictly requires boolean inputs, so cast if not already boolean.
1267    """
1268    return self.sql(
1269        exp.EQ(
1270            this=exp.CountIf(this=_cast_to_boolean(expression.this)),
1271            expression=exp.Literal.number(1),
1272        )
1273    )
1274
1275
1276def _bitshift_sql(
1277    self: DuckDBGenerator, expression: exp.BitwiseLeftShift | exp.BitwiseRightShift
1278) -> str:
1279    """
1280    Transform bitshift expressions for DuckDB by injecting BIT/INT128 casts.
1281
1282    DuckDB's bitwise shift operators don't work with BLOB/BINARY types, so we cast
1283    them to BIT for the operation, then cast the result back to the original type.
1284
1285    Note: Assumes type annotation has been applied with the source dialect.
1286    """
1287    operator = "<<" if isinstance(expression, exp.BitwiseLeftShift) else ">>"
1288    result_is_blob = False
1289    this = expression.this
1290
1291    if _is_binary(this):
1292        result_is_blob = True
1293        expression.set("this", exp.cast(this, exp.DType.BIT))
1294    elif expression.args.get("requires_int128"):
1295        this.replace(exp.cast(this, exp.DType.INT128))
1296
1297    result_sql = self.binary(expression, operator)
1298
1299    # Wrap in parentheses if parent is a bitwise operator to "fix" DuckDB precedence issue
1300    # DuckDB parses: a << b | c << d  as  (a << b | c) << d
1301    if isinstance(expression.parent, exp.Binary):
1302        result_sql = self.sql(exp.Paren(this=result_sql))
1303
1304    if result_is_blob:
1305        result_sql = self.sql(
1306            exp.Cast(this=result_sql, to=exp.DataType.from_str("BLOB", dialect="duckdb"))
1307        )
1308
1309    return result_sql
1310
1311
1312def _scale_rounding_sql(
1313    self: DuckDBGenerator,
1314    expression: exp.Expr,
1315    rounding_func: Type[exp.Expr],
1316) -> str | None:
1317    """
1318    Handle scale parameter transformation for rounding functions.
1319
1320    DuckDB doesn't support the scale parameter for certain functions (e.g., FLOOR, CEIL),
1321    so we transform: FUNC(x, n) to ROUND(FUNC(x * 10^n) / 10^n, n)
1322
1323    Args:
1324        self: The DuckDB generator instance
1325        expression: The expression to transform (must have 'this', 'decimals', and 'to' args)
1326        rounding_func: The rounding function class to use in the transformation
1327
1328    Returns:
1329        The transformed SQL string if decimals parameter exists, None otherwise
1330    """
1331    decimals = expression.args.get("decimals")
1332
1333    if decimals is None or expression.args.get("to") is not None:
1334        return None
1335
1336    this = expression.this
1337    if isinstance(this, exp.Binary):
1338        this = exp.Paren(this=this)
1339
1340    n_int = decimals
1341    if not (decimals.is_int or decimals.is_type(*exp.DataType.INTEGER_TYPES)):
1342        n_int = exp.cast(decimals, exp.DType.INT)
1343
1344    pow_ = exp.Pow(this=exp.Literal.number("10"), expression=n_int)
1345    rounded = rounding_func(this=exp.Mul(this=this, expression=pow_))
1346    result = exp.Div(this=rounded, expression=pow_.copy())
1347
1348    return self.round_sql(
1349        exp.Round(this=result, decimals=decimals, casts_non_integer_decimals=True)
1350    )
1351
1352
1353def _ceil_floor(self: DuckDBGenerator, expression: exp.Floor | exp.Ceil) -> str:
1354    scaled_sql = _scale_rounding_sql(self, expression, type(expression))
1355    if scaled_sql is not None:
1356        return scaled_sql
1357    return self.ceil_floor(expression)
1358
1359
1360def _regr_val_sql(
1361    self: DuckDBGenerator,
1362    expression: exp.RegrValx | exp.RegrValy,
1363) -> str:
1364    """
1365    Transpile Snowflake's REGR_VALX/REGR_VALY to DuckDB equivalent.
1366
1367    REGR_VALX(y, x) returns NULL if y is NULL; otherwise returns x.
1368    REGR_VALY(y, x) returns NULL if x is NULL; otherwise returns y.
1369    """
1370    from sqlglot.optimizer.annotate_types import annotate_types
1371
1372    y = expression.this
1373    x = expression.expression
1374
1375    # Determine which argument to check for NULL and which to return based on expression type
1376    if isinstance(expression, exp.RegrValx):
1377        # REGR_VALX: check y for NULL, return x
1378        check_for_null = y
1379        return_value = x
1380        return_value_attr = "expression"
1381    else:
1382        # REGR_VALY: check x for NULL, return y
1383        check_for_null = x
1384        return_value = y
1385        return_value_attr = "this"
1386
1387    # Get the type from the return argument
1388    result_type = return_value.type
1389
1390    # If no type info, annotate the expression to infer types
1391    if not result_type or result_type.this == exp.DType.UNKNOWN:
1392        try:
1393            annotated = annotate_types(expression.copy(), dialect=self.dialect)
1394            result_type = getattr(annotated, return_value_attr).type
1395        except Exception:
1396            pass
1397
1398    # Default to DOUBLE for regression functions if type still unknown
1399    if not result_type or result_type.this == exp.DType.UNKNOWN:
1400        result_type = exp.DType.DOUBLE.into_expr()
1401
1402    # Cast NULL to the same type as return_value to avoid DuckDB type inference issues
1403    typed_null = exp.Cast(this=exp.Null(), to=result_type)
1404
1405    return self.sql(
1406        exp.If(
1407            this=exp.Is(this=check_for_null.copy(), expression=exp.Null()),
1408            true=typed_null,
1409            false=return_value.copy(),
1410        )
1411    )
1412
1413
1414def _maybe_corr_null_to_false(
1415    expression: exp.Filter | exp.Window | exp.Corr,
1416) -> exp.Filter | exp.Window | exp.Corr | None:
1417    corr = expression
1418    while isinstance(corr, (exp.Window, exp.Filter)):
1419        corr = corr.this
1420
1421    if not isinstance(corr, exp.Corr) or not corr.args.get("null_on_zero_variance"):
1422        return None
1423
1424    corr.set("null_on_zero_variance", False)
1425    return expression
1426
1427
1428def _date_from_parts_sql(self, expression: exp.DateFromParts) -> str:
1429    """
1430    Snowflake's DATE_FROM_PARTS allows out-of-range values for the month and day input.
1431    E.g., larger values (month=13, day=100), zero-values (month=0, day=0), negative values (month=-13, day=-100).
1432
1433    DuckDB's MAKE_DATE does not support out-of-range values, but DuckDB's INTERVAL type does.
1434
1435    We convert to date arithmetic:
1436    DATE_FROM_PARTS(year, month, day)
1437    - MAKE_DATE(year, 1, 1) + INTERVAL (month-1) MONTH + INTERVAL (day-1) DAY
1438    """
1439    year_expr = expression.args.get("year")
1440    month_expr = expression.args.get("month")
1441    day_expr = expression.args.get("day")
1442
1443    if expression.args.get("allow_overflow"):
1444        base_date: exp.Expr = exp.func(
1445            "MAKE_DATE", year_expr, exp.Literal.number(1), exp.Literal.number(1)
1446        )
1447
1448        if month_expr:
1449            base_date = base_date + exp.Interval(this=month_expr - 1, unit=exp.var("MONTH"))
1450
1451        if day_expr:
1452            base_date = base_date + exp.Interval(this=day_expr - 1, unit=exp.var("DAY"))
1453
1454        return self.sql(exp.cast(expression=base_date, to=exp.DType.DATE))
1455
1456    return self.func("MAKE_DATE", year_expr, month_expr, day_expr)
1457
1458
1459def _round_arg(arg: exp.Expr, round_input: bool | None = None) -> exp.Expr:
1460    if round_input:
1461        return exp.func("ROUND", arg, exp.Literal.number(0))
1462    return arg
1463
1464
1465def _boolnot_sql(self: DuckDBGenerator, expression: exp.Boolnot) -> str:
1466    arg = _round_arg(expression.this, expression.args.get("round_input"))
1467    return self.sql(exp.not_(exp.paren(arg)))
1468
1469
1470def _booland_sql(self: DuckDBGenerator, expression: exp.Booland) -> str:
1471    round_input = expression.args.get("round_input")
1472    left = _round_arg(expression.this, round_input)
1473    right = _round_arg(expression.expression, round_input)
1474    return self.sql(exp.paren(exp.and_(exp.paren(left), exp.paren(right), wrap=False)))
1475
1476
1477def _boolor_sql(self: DuckDBGenerator, expression: exp.Boolor) -> str:
1478    round_input = expression.args.get("round_input")
1479    left = _round_arg(expression.this, round_input)
1480    right = _round_arg(expression.expression, round_input)
1481    return self.sql(exp.paren(exp.or_(exp.paren(left), exp.paren(right), wrap=False)))
1482
1483
1484def _xor_sql(self: DuckDBGenerator, expression: exp.Xor) -> str:
1485    round_input = expression.args.get("round_input")
1486    left = _round_arg(expression.this, round_input)
1487    right = _round_arg(expression.expression, round_input)
1488    return self.sql(
1489        exp.or_(
1490            exp.paren(exp.and_(left.copy(), exp.paren(right.not_()), wrap=False)),
1491            exp.paren(exp.and_(exp.paren(left.not_()), right.copy(), wrap=False)),
1492            wrap=False,
1493        )
1494    )
1495
1496
1497def _explode_to_unnest_sql(self: DuckDBGenerator, expression: exp.Lateral) -> str:
1498    """Handle LATERAL VIEW EXPLODE/INLINE conversion to UNNEST for DuckDB."""
1499    explode = expression.this
1500
1501    if isinstance(explode, exp.Inline):
1502        # For INLINE, create CROSS JOIN LATERAL (SELECT UNNEST(..., max_depth => 2))
1503        # Build the UNNEST call with DuckDB-style named parameter
1504        unnest_expr = exp.Unnest(
1505            expressions=[
1506                explode.this,
1507                exp.Kwarg(this=exp.var("max_depth"), expression=exp.Literal.number(2)),
1508            ]
1509        )
1510        select_expr = exp.Select(expressions=[unnest_expr]).subquery()
1511
1512        alias_expr = expression.args.get("alias")
1513        if alias_expr and not alias_expr.this:
1514            # we need to provide a table name if not present
1515            alias_expr.set("this", exp.to_identifier(f"_u_{expression.index}"))
1516
1517        transformed_lateral_expr = exp.Lateral(this=select_expr, alias=alias_expr)
1518        cross_join_lateral_expr = exp.Join(this=transformed_lateral_expr, kind="CROSS")
1519
1520        return self.sql(cross_join_lateral_expr)
1521
1522    # For other cases, use the standard conversion
1523    return explode_to_unnest_sql(self, expression)
1524
1525
1526def _sha_sql(
1527    self: DuckDBGenerator,
1528    expression: exp.Expr,
1529    hash_func: str,
1530    is_binary: bool = False,
1531) -> str:
1532    arg = expression.this
1533
1534    # For SHA2 variants, check digest length (DuckDB only supports SHA256)
1535    if hash_func == "SHA256":
1536        length = expression.text("length") or "256"
1537        if length != "256":
1538            self.unsupported("DuckDB only supports SHA256 hashing algorithm.")
1539
1540    # Cast if type is incompatible with DuckDB
1541    if (
1542        arg.type
1543        and arg.type.this != exp.DType.UNKNOWN
1544        and not arg.is_type(*exp.DataType.TEXT_TYPES)
1545        and not _is_binary(arg)
1546    ):
1547        arg = exp.cast(arg, exp.DType.VARCHAR)
1548
1549    result = self.func(hash_func, arg)
1550    return self.func("UNHEX", result) if is_binary else result
1551
1552
1553class DuckDBGenerator(generator.Generator):
1554    PARAMETER_TOKEN = "$"
1555    NAMED_PLACEHOLDER_TOKEN = "$"
1556    JOIN_HINTS = False
1557    TABLE_HINTS = False
1558    QUERY_HINTS = False
1559    LIMIT_FETCH = "LIMIT"
1560    STRUCT_DELIMITER = ("(", ")")
1561    RENAME_TABLE_WITH_DB = False
1562    NVL2_SUPPORTED = False
1563    SEMI_ANTI_JOIN_WITH_SIDE = False
1564    TABLESAMPLE_KEYWORDS = "USING SAMPLE"
1565    TABLESAMPLE_SEED_KEYWORD = "REPEATABLE"
1566    LAST_DAY_SUPPORTS_DATE_PART = False
1567    JSON_KEY_VALUE_PAIR_SEP = ","
1568    IGNORE_NULLS_IN_FUNC = True
1569    IGNORE_NULLS_BEFORE_ORDER = False
1570    JSON_PATH_BRACKETED_KEY_SUPPORTED = False
1571    SUPPORTS_CREATE_TABLE_LIKE = False
1572    MULTI_ARG_DISTINCT = False
1573    CAN_IMPLEMENT_ARRAY_ANY = True
1574    SUPPORTS_TO_NUMBER = False
1575    SELECT_KINDS: tuple[str, ...] = ()
1576    SUPPORTS_DECODE_CASE = False
1577    SUPPORTS_DROP_ALTER_ICEBERG_PROPERTY = False
1578
1579    AFTER_HAVING_MODIFIER_TRANSFORMS = generator.AFTER_HAVING_MODIFIER_TRANSFORMS
1580    SUPPORTS_WINDOW_EXCLUDE = True
1581    COPY_HAS_INTO_KEYWORD = False
1582    STAR_EXCEPT = "EXCLUDE"
1583    PAD_FILL_PATTERN_IS_REQUIRED = True
1584    ARRAY_SIZE_DIM_REQUIRED: bool | None = False
1585    NORMALIZE_EXTRACT_DATE_PARTS = True
1586    SUPPORTS_LIKE_QUANTIFIERS = False
1587    HISTORICAL_DATA_POST_ALIAS = True
1588    SET_ASSIGNMENT_REQUIRES_VARIABLE_KEYWORD = True
1589
1590    TRANSFORMS = {
1591        **generator.Generator.TRANSFORMS,
1592        exp.AnyValue: _anyvalue_sql,
1593        exp.ApproxDistinct: approx_count_distinct_sql,
1594        exp.Boolnot: _boolnot_sql,
1595        exp.Booland: _booland_sql,
1596        exp.Boolor: _boolor_sql,
1597        exp.Array: transforms.preprocess(
1598            [transforms.inherit_struct_field_names],
1599            generator=inline_array_unless_query,
1600        ),
1601        exp.ArrayAppend: array_append_sql("LIST_APPEND"),
1602        exp.ArrayCompact: array_compact_sql,
1603        exp.ArrayConstructCompact: lambda self, e: self.sql(
1604            exp.ArrayCompact(this=exp.Array(expressions=e.expressions))
1605        ),
1606        exp.ArrayConcat: array_concat_sql("LIST_CONCAT"),
1607        exp.ArrayContains: _array_contains_sql,
1608        exp.ArrayOverlaps: _array_overlaps_sql,
1609        exp.ArrayFilter: rename_func("LIST_FILTER"),
1610        exp.ArrayInsert: _array_insert_sql,
1611        exp.ArrayPosition: lambda self, e: (
1612            self.sql(
1613                exp.Sub(
1614                    this=exp.ArrayPosition(this=e.this, expression=e.expression),
1615                    expression=exp.Literal.number(1),
1616                )
1617            )
1618            if e.args.get("zero_based")
1619            else self.func("ARRAY_POSITION", e.this, e.expression)
1620        ),
1621        exp.ArrayRemoveAt: _array_remove_at_sql,
1622        exp.ArrayRemove: remove_from_array_using_filter,
1623        exp.ArraySort: _array_sort_sql,
1624        exp.ArrayPrepend: array_append_sql("LIST_PREPEND", swap_params=True),
1625        exp.ArraySum: rename_func("LIST_SUM"),
1626        exp.ArrayMax: rename_func("LIST_MAX"),
1627        exp.ArrayMin: rename_func("LIST_MIN"),
1628        exp.Base64DecodeBinary: lambda self, e: _base64_decode_sql(self, e, to_string=False),
1629        exp.Base64DecodeString: lambda self, e: _base64_decode_sql(self, e, to_string=True),
1630        exp.BitwiseAnd: lambda self, e: self._bitwise_op(e, "&"),
1631        exp.BitwiseAndAgg: _bitwise_agg_sql,
1632        exp.BitwiseCount: rename_func("BIT_COUNT"),
1633        exp.BitwiseLeftShift: _bitshift_sql,
1634        exp.BitwiseOr: lambda self, e: self._bitwise_op(e, "|"),
1635        exp.BitwiseOrAgg: _bitwise_agg_sql,
1636        exp.BitwiseRightShift: _bitshift_sql,
1637        exp.BitwiseXorAgg: _bitwise_agg_sql,
1638        exp.CommentColumnConstraint: no_comment_column_constraint_sql,
1639        exp.Corr: lambda self, e: self._corr_sql(e),
1640        exp.CosineDistance: rename_func("LIST_COSINE_DISTANCE"),
1641        exp.CurrentTime: lambda *_: "CURRENT_TIME",
1642        exp.CurrentSchemas: lambda self, e: self.func(
1643            "current_schemas", e.this if e.this else exp.true()
1644        ),
1645        exp.CurrentTimestamp: lambda self, e: (
1646            self.sql(
1647                exp.AtTimeZone(this=exp.var("CURRENT_TIMESTAMP"), zone=exp.Literal.string("UTC"))
1648            )
1649            if e.args.get("sysdate")
1650            else "CURRENT_TIMESTAMP"
1651        ),
1652        exp.CurrentVersion: rename_func("version"),
1653        exp.Localtime: unsupported_args("this")(lambda *_: "LOCALTIME"),
1654        exp.DayOfMonth: rename_func("DAYOFMONTH"),
1655        exp.DayOfWeek: rename_func("DAYOFWEEK"),
1656        exp.DayOfWeekIso: rename_func("ISODOW"),
1657        exp.DayOfYear: rename_func("DAYOFYEAR"),
1658        exp.Dayname: lambda self, e: (
1659            self.func("STRFTIME", e.this, exp.Literal.string("%a"))
1660            if e.args.get("abbreviated")
1661            else self.func("DAYNAME", e.this)
1662        ),
1663        exp.Monthname: lambda self, e: (
1664            self.func("STRFTIME", e.this, exp.Literal.string("%b"))
1665            if e.args.get("abbreviated")
1666            else self.func("MONTHNAME", e.this)
1667        ),
1668        exp.DataType: _datatype_sql,
1669        exp.Date: _date_sql,
1670        exp.DateAdd: _date_delta_to_binary_interval_op(),
1671        exp.DateFromParts: _date_from_parts_sql,
1672        exp.DateSub: _date_delta_to_binary_interval_op(),
1673        exp.DateDiff: _date_diff_sql,
1674        exp.DateStrToDate: datestrtodate_sql,
1675        exp.Datetime: no_datetime_sql,
1676        exp.DatetimeDiff: _date_diff_sql,
1677        exp.DatetimeSub: _date_delta_to_binary_interval_op(),
1678        exp.DatetimeAdd: _date_delta_to_binary_interval_op(),
1679        exp.DateToDi: lambda self, e: (
1680            f"CAST(STRFTIME({self.sql(e, 'this')}, {self.dialect.DATEINT_FORMAT}) AS INT)"
1681        ),
1682        exp.Decode: lambda self, e: encode_decode_sql(self, e, "DECODE", replace=False),
1683        exp.HexDecodeString: lambda self, e: self.sql(exp.Decode(this=exp.Unhex(this=e.this))),
1684        exp.DiToDate: lambda self, e: (
1685            f"CAST(STRPTIME(CAST({self.sql(e, 'this')} AS TEXT), {self.dialect.DATEINT_FORMAT}) AS DATE)"
1686        ),
1687        exp.Encode: lambda self, e: encode_decode_sql(self, e, "ENCODE", replace=False),
1688        exp.EqualNull: lambda self, e: self.sql(
1689            exp.NullSafeEQ(this=e.this, expression=e.expression)
1690        ),
1691        exp.EuclideanDistance: rename_func("LIST_DISTANCE"),
1692        exp.GenerateDateArray: _generate_datetime_array_sql,
1693        exp.GenerateSeries: generate_series_sql("GENERATE_SERIES", "RANGE"),
1694        exp.GenerateTimestampArray: _generate_datetime_array_sql,
1695        exp.Getbit: getbit_sql,
1696        exp.GroupConcat: lambda self, e: groupconcat_sql(self, e, within_group=False),
1697        exp.Explode: rename_func("UNNEST"),
1698        exp.IcebergProperty: lambda *_: "",
1699        exp.IntDiv: lambda self, e: self.binary(e, "//"),
1700        exp.IsInf: rename_func("ISINF"),
1701        exp.IsNan: rename_func("ISNAN"),
1702        exp.IsNullValue: lambda self, e: self.sql(
1703            exp.func("JSON_TYPE", e.this).eq(exp.Literal.string("NULL"))
1704        ),
1705        exp.IsArray: lambda self, e: self.sql(
1706            exp.func("JSON_TYPE", e.this).eq(exp.Literal.string("ARRAY"))
1707        ),
1708        exp.Ceil: _ceil_floor,
1709        exp.Floor: _ceil_floor,
1710        exp.JSONBExists: rename_func("JSON_EXISTS"),
1711        exp.JSONExtract: _arrow_json_extract_sql,
1712        exp.JSONExtractArray: _json_extract_value_array_sql,
1713        exp.JSONFormat: _json_format_sql,
1714        exp.JSONValueArray: _json_extract_value_array_sql,
1715        exp.Lateral: _explode_to_unnest_sql,
1716        exp.LogicalOr: lambda self, e: self.func("BOOL_OR", _cast_to_boolean(e.this)),
1717        exp.LogicalAnd: lambda self, e: self.func("BOOL_AND", _cast_to_boolean(e.this)),
1718        exp.Select: transforms.preprocess(
1719            [connect_by_to_recursive_cte, _seq_to_range_in_generator]
1720        ),
1721        exp.Seq1: lambda self, e: _seq_sql(self, e, 1),
1722        exp.Seq2: lambda self, e: _seq_sql(self, e, 2),
1723        exp.Seq4: lambda self, e: _seq_sql(self, e, 4),
1724        exp.Seq8: lambda self, e: _seq_sql(self, e, 8),
1725        exp.BoolxorAgg: _boolxor_agg_sql,
1726        exp.MakeInterval: lambda self, e: no_make_interval_sql(self, e, sep=" "),
1727        exp.Initcap: _initcap_sql,
1728        exp.MD5Digest: lambda self, e: self.func("UNHEX", self.func("MD5", e.this)),
1729        exp.SHA: lambda self, e: _sha_sql(self, e, "SHA1"),
1730        exp.SHA1Digest: lambda self, e: _sha_sql(self, e, "SHA1", is_binary=True),
1731        exp.SHA2: lambda self, e: _sha_sql(self, e, "SHA256"),
1732        exp.SHA2Digest: lambda self, e: _sha_sql(self, e, "SHA256", is_binary=True),
1733        exp.MonthsBetween: months_between_sql,
1734        exp.NextDay: _day_navigation_sql,
1735        exp.PercentileCont: rename_func("QUANTILE_CONT"),
1736        exp.PercentileDisc: rename_func("QUANTILE_DISC"),
1737        # DuckDB doesn't allow qualified columns inside of PIVOT expressions.
1738        # See: https://github.com/duckdb/duckdb/blob/671faf92411182f81dce42ac43de8bfb05d9909e/src/planner/binder/tableref/bind_pivot.cpp#L61-L62
1739        exp.Pivot: transforms.preprocess([transforms.unqualify_columns]),
1740        exp.PreviousDay: _day_navigation_sql,
1741        exp.RegexpILike: lambda self, e: self.func(
1742            "REGEXP_MATCHES", e.this, e.expression, exp.Literal.string("i")
1743        ),
1744        exp.RegexpSplit: rename_func("STR_SPLIT_REGEX"),
1745        exp.RegrValx: _regr_val_sql,
1746        exp.RegrValy: _regr_val_sql,
1747        exp.Return: lambda self, e: self.sql(e, "this"),
1748        exp.ReturnsProperty: lambda self, e: "TABLE" if isinstance(e.this, exp.Schema) else "",
1749        exp.StrToUnix: lambda self, e: self.func(
1750            "EPOCH", self.func("STRPTIME", e.this, self.format_time(e))
1751        ),
1752        exp.Struct: _struct_sql,
1753        exp.Transform: rename_func("LIST_TRANSFORM"),
1754        exp.TimeAdd: _date_delta_to_binary_interval_op(),
1755        exp.TimeSub: _date_delta_to_binary_interval_op(),
1756        exp.Time: no_time_sql,
1757        exp.TimeDiff: _timediff_sql,
1758        exp.Timestamp: no_timestamp_sql,
1759        exp.TimestampAdd: _date_delta_to_binary_interval_op(),
1760        exp.TimestampDiff: lambda self, e: self.func(
1761            "DATE_DIFF", exp.Literal.string(e.unit), e.expression, e.this
1762        ),
1763        exp.TimestampSub: _date_delta_to_binary_interval_op(),
1764        exp.TimeStrToDate: lambda self, e: self.sql(exp.cast(e.this, exp.DType.DATE)),
1765        exp.TimeStrToTime: timestrtotime_sql,
1766        exp.TimeStrToUnix: lambda self, e: self.func(
1767            "EPOCH", exp.cast(e.this, exp.DType.TIMESTAMP)
1768        ),
1769        exp.TimeToStr: lambda self, e: self.func("STRFTIME", e.this, self.format_time(e)),
1770        exp.ToBoolean: _to_boolean_sql,
1771        exp.ToVariant: lambda self, e: self.sql(
1772            exp.cast(e.this, exp.DataType.from_str("VARIANT", dialect="duckdb"))
1773        ),
1774        exp.TimeToUnix: rename_func("EPOCH"),
1775        exp.TsOrDiToDi: lambda self, e: (
1776            f"CAST(SUBSTR(REPLACE(CAST({self.sql(e, 'this')} AS TEXT), '-', ''), 1, 8) AS INT)"
1777        ),
1778        exp.TsOrDsAdd: _date_delta_to_binary_interval_op(),
1779        exp.TsOrDsDiff: lambda self, e: self.func(
1780            "DATE_DIFF",
1781            f"'{e.args.get('unit') or 'DAY'}'",
1782            exp.cast(e.expression, exp.DType.TIMESTAMP),
1783            exp.cast(e.this, exp.DType.TIMESTAMP),
1784        ),
1785        exp.UnixMicros: lambda self, e: self.func("EPOCH_US", _implicit_datetime_cast(e.this)),
1786        exp.UnixMillis: lambda self, e: self.func("EPOCH_MS", _implicit_datetime_cast(e.this)),
1787        exp.UnixSeconds: lambda self, e: self.sql(
1788            exp.cast(self.func("EPOCH", _implicit_datetime_cast(e.this)), exp.DType.BIGINT)
1789        ),
1790        exp.UnixToStr: lambda self, e: self.func(
1791            "STRFTIME", self.func("TO_TIMESTAMP", e.this), self.format_time(e)
1792        ),
1793        exp.UnixToTime: _unix_to_time_sql,
1794        exp.UnixToTimeStr: lambda self, e: f"CAST(TO_TIMESTAMP({self.sql(e, 'this')}) AS TEXT)",
1795        exp.VariancePop: rename_func("VAR_POP"),
1796        exp.WeekOfYear: rename_func("WEEKOFYEAR"),
1797        exp.YearOfWeek: lambda self, e: self.sql(
1798            exp.Extract(
1799                this=exp.Var(this="ISOYEAR"),
1800                expression=e.this,
1801            )
1802        ),
1803        exp.YearOfWeekIso: lambda self, e: self.sql(
1804            exp.Extract(
1805                this=exp.Var(this="ISOYEAR"),
1806                expression=e.this,
1807            )
1808        ),
1809        exp.Xor: _xor_sql,
1810        exp.JSONObjectAgg: rename_func("JSON_GROUP_OBJECT"),
1811        exp.JSONBObjectAgg: rename_func("JSON_GROUP_OBJECT"),
1812        exp.DateBin: rename_func("TIME_BUCKET"),
1813        exp.LastDay: _last_day_sql,
1814    }
1815
1816    SUPPORTED_JSON_PATH_PARTS = {
1817        exp.JSONPathKey,
1818        exp.JSONPathRoot,
1819        exp.JSONPathSubscript,
1820        exp.JSONPathWildcard,
1821    }
1822
1823    TYPE_MAPPING = {
1824        **generator.Generator.TYPE_MAPPING,
1825        exp.DType.BINARY: "BLOB",
1826        exp.DType.BPCHAR: "TEXT",
1827        exp.DType.CHAR: "TEXT",
1828        exp.DType.DATETIME: "TIMESTAMP",
1829        exp.DType.DECFLOAT: "DECIMAL",
1830        exp.DType.FLOAT: "REAL",
1831        exp.DType.JSONB: "JSON",
1832        exp.DType.NCHAR: "TEXT",
1833        exp.DType.NVARCHAR: "TEXT",
1834        exp.DType.UINT: "UINTEGER",
1835        exp.DType.VARBINARY: "BLOB",
1836        exp.DType.ROWVERSION: "BLOB",
1837        exp.DType.VARCHAR: "TEXT",
1838        exp.DType.TIMESTAMPLTZ: "TIMESTAMPTZ",
1839        exp.DType.TIMESTAMPNTZ: "TIMESTAMP",
1840        exp.DType.TIMESTAMP_S: "TIMESTAMP_S",
1841        exp.DType.TIMESTAMP_MS: "TIMESTAMP_MS",
1842        exp.DType.TIMESTAMP_NS: "TIMESTAMP_NS",
1843        exp.DType.BIGDECIMAL: "DECIMAL",
1844    }
1845
1846    TYPE_PARAM_SETTINGS = {
1847        **generator.Generator.TYPE_PARAM_SETTINGS,
1848        exp.DType.BIGDECIMAL: ((38, 5), (38, 38)),
1849        exp.DType.DECFLOAT: ((38, 5), (38, 38)),
1850    }
1851
1852    # https://github.com/duckdb/duckdb/blob/ff7f24fd8e3128d94371827523dae85ebaf58713/third_party/libpg_query/grammar/keywords/reserved_keywords.list#L1-L77
1853    RESERVED_KEYWORDS = {
1854        "array",
1855        "analyse",
1856        "union",
1857        "all",
1858        "when",
1859        "in_p",
1860        "default",
1861        "create_p",
1862        "window",
1863        "asymmetric",
1864        "to",
1865        "else",
1866        "localtime",
1867        "from",
1868        "end_p",
1869        "select",
1870        "current_date",
1871        "foreign",
1872        "with",
1873        "grant",
1874        "session_user",
1875        "or",
1876        "except",
1877        "references",
1878        "fetch",
1879        "limit",
1880        "group_p",
1881        "leading",
1882        "into",
1883        "collate",
1884        "offset",
1885        "do",
1886        "then",
1887        "localtimestamp",
1888        "check_p",
1889        "lateral_p",
1890        "current_role",
1891        "where",
1892        "asc_p",
1893        "placing",
1894        "desc_p",
1895        "user",
1896        "unique",
1897        "initially",
1898        "column",
1899        "both",
1900        "some",
1901        "as",
1902        "any",
1903        "only",
1904        "deferrable",
1905        "null_p",
1906        "current_time",
1907        "true_p",
1908        "table",
1909        "case",
1910        "trailing",
1911        "variadic",
1912        "for",
1913        "on",
1914        "distinct",
1915        "false_p",
1916        "not",
1917        "constraint",
1918        "current_timestamp",
1919        "returning",
1920        "primary",
1921        "intersect",
1922        "having",
1923        "analyze",
1924        "current_user",
1925        "and",
1926        "cast",
1927        "symmetric",
1928        "using",
1929        "order",
1930        "current_catalog",
1931    }
1932
1933    UNWRAPPED_INTERVAL_VALUES = (exp.Literal, exp.Paren)
1934
1935    # DuckDB doesn't generally support CREATE TABLE .. properties
1936    # https://duckdb.org/docs/sql/statements/create_table.html
1937    # There are a few exceptions (e.g. temporary tables) which are supported or
1938    # can be transpiled to DuckDB, so we explicitly override them accordingly
1939    PROPERTIES_LOCATION = {
1940        **{
1941            prop: exp.Properties.Location.UNSUPPORTED
1942            for prop in generator.Generator.PROPERTIES_LOCATION
1943        },
1944        exp.LikeProperty: exp.Properties.Location.POST_SCHEMA,
1945        exp.TemporaryProperty: exp.Properties.Location.POST_CREATE,
1946        exp.ReturnsProperty: exp.Properties.Location.POST_ALIAS,
1947        exp.SequenceProperties: exp.Properties.Location.POST_EXPRESSION,
1948        exp.IcebergProperty: exp.Properties.Location.POST_CREATE,
1949    }
1950
1951    IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS: t.ClassVar = _IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS
1952
1953    # Template for ZIPF transpilation - placeholders get replaced with actual parameters
1954    ZIPF_TEMPLATE: exp.Expr = exp.maybe_parse(
1955        """
1956        WITH rand AS (SELECT :random_expr AS r),
1957        weights AS (
1958            SELECT i, 1.0 / POWER(i, :s) AS w
1959            FROM RANGE(1, :n + 1) AS t(i)
1960        ),
1961        cdf AS (
1962            SELECT i, SUM(w) OVER (ORDER BY i) / SUM(w) OVER () AS p
1963            FROM weights
1964        )
1965        SELECT MIN(i)
1966        FROM cdf
1967        WHERE p >= (SELECT r FROM rand)
1968        """
1969    )
1970
1971    # Template for NORMAL transpilation using Box-Muller transform
1972    # mean + (stddev * sqrt(-2 * ln(u1)) * cos(2 * pi * u2))
1973    NORMAL_TEMPLATE: exp.Expr = exp.maybe_parse(
1974        ":mean + (:stddev * SQRT(-2 * LN(GREATEST(:u1, 1e-10))) * COS(2 * PI() * :u2))"
1975    )
1976
1977    # Template for generating a seeded pseudo-random value in [0, 1) from a hash
1978    SEEDED_RANDOM_TEMPLATE: exp.Expr = exp.maybe_parse("(ABS(HASH(:seed)) % 1000000) / 1000000.0")
1979
1980    # Template for generating signed and unsigned SEQ values within a specified range
1981    SEQ_UNSIGNED: exp.Expr = _SEQ_UNSIGNED
1982    SEQ_SIGNED: exp.Expr = _SEQ_SIGNED
1983
1984    # Template for MAP_CAT transpilation - Snowflake semantics:
1985    # 1. Returns NULL if either input is NULL
1986    # 2. For duplicate keys, prefers non-NULL value (COALESCE(m2[k], m1[k]))
1987    # 3. Filters out entries with NULL values from the result
1988    MAPCAT_TEMPLATE: exp.Expr = exp.maybe_parse(
1989        """
1990        CASE
1991            WHEN :map1 IS NULL OR :map2 IS NULL THEN NULL
1992            ELSE MAP_FROM_ENTRIES(LIST_FILTER(LIST_TRANSFORM(
1993                LIST_DISTINCT(LIST_CONCAT(MAP_KEYS(:map1), MAP_KEYS(:map2))),
1994                __k -> STRUCT_PACK(key := __k, value := COALESCE(:map2[__k], :map1[__k]))
1995            ), __x -> __x.value IS NOT NULL))
1996        END
1997        """
1998    )
1999
2000    # Mappings for EXTRACT/DATE_PART transpilation
2001    # Maps Snowflake specifiers unsupported in DuckDB to strftime format codes
2002    EXTRACT_STRFTIME_MAPPINGS: dict[str, tuple[str, str]] = {
2003        "WEEKISO": ("%V", "INTEGER"),
2004        "YEAROFWEEK": ("%G", "INTEGER"),
2005        "YEAROFWEEKISO": ("%G", "INTEGER"),
2006        "NANOSECOND": ("%n", "BIGINT"),
2007    }
2008
2009    # Maps epoch-based specifiers to DuckDB epoch functions
2010    EXTRACT_EPOCH_MAPPINGS: dict[str, str] = {
2011        "EPOCH_SECOND": "EPOCH",
2012        "EPOCH_MILLISECOND": "EPOCH_MS",
2013        "EPOCH_MICROSECOND": "EPOCH_US",
2014        "EPOCH_NANOSECOND": "EPOCH_NS",
2015    }
2016
2017    # Template for BITMAP_CONSTRUCT_AGG transpilation
2018    #
2019    # BACKGROUND:
2020    # Snowflake's BITMAP_CONSTRUCT_AGG aggregates integers into a compact binary bitmap.
2021    # Supports values in range 0-32767, this version returns NULL if any value is out of range
2022    # See: https://docs.snowflake.com/en/sql-reference/functions/bitmap_construct_agg
2023    # See: https://docs.snowflake.com/en/user-guide/querying-bitmaps-for-distinct-counts
2024    #
2025    # Snowflake uses two different formats based on the number of unique values:
2026    #
2027    # Format 1 - Small bitmap (< 5 unique values): Length of 10 bytes
2028    #   Bytes 0-1: Count of values as 2-byte big-endian integer (e.g., 3 values = 0x0003)
2029    #   Bytes 2-9: Up to 4 values, each as 2-byte little-endian integers, zero-padded to 8 bytes
2030    #   Example: Values [1, 2, 3] -> 0x0003 0100 0200 0300 0000 (hex)
2031    #                                count  v1   v2   v3   pad
2032    #
2033    # Format 2 - Large bitmap (>= 5 unique values): Length of 10 + (2 * count) bytes
2034    #   Bytes 0-9: Fixed header 0x08 followed by 9 zero bytes
2035    #   Bytes 10+: Each value as 2-byte little-endian integer (no padding)
2036    #   Example: Values [1,2,3,4,5] -> 0x08 00000000 00000000 00 0100 0200 0300 0400 0500
2037    #                                  hdr  ----9 zero bytes----  v1   v2   v3   v4   v5
2038    #
2039    # TEMPLATE STRUCTURE
2040    #
2041    # Phase 1 - Innermost subquery: Data preparation
2042    #   SELECT LIST_SORT(...) AS l
2043    #   - Aggregates all input values into a list, remove NULLs, duplicates and sorts
2044    #   Result: Clean, sorted list of unique non-null integers stored as 'l'
2045    #
2046    # Phase 2 - Middle subquery: Hex string construction
2047    #   LIST_TRANSFORM(...)
2048    #   - Converts each integer to 2-byte little-endian hex representation
2049    #   - & 255 extracts low byte, >> 8 extracts high byte
2050    #   - LIST_REDUCE: Concatenates all hex pairs into single string 'h'
2051    #   Result: Hex string of all values
2052    #
2053    # Phase 3 - Outer SELECT: Final bitmap assembly
2054    #   LENGTH(l) < 5:
2055    #   - Small format: 2-byte count (big-endian via %04X) + values + zero padding
2056    #   LENGTH(l) >= 5:
2057    #   - Large format: Fixed 10-byte header + values (no padding needed)
2058    #   Result: Complete binary bitmap as BLOB
2059    #
2060    BITMAP_CONSTRUCT_AGG_TEMPLATE: exp.Expr = exp.maybe_parse(
2061        """
2062        SELECT CASE
2063            WHEN l IS NULL OR LENGTH(l) = 0 THEN NULL
2064            WHEN LENGTH(l) != LENGTH(LIST_FILTER(l, __v -> __v BETWEEN 0 AND 32767)) THEN NULL
2065            WHEN LENGTH(l) < 5 THEN UNHEX(PRINTF('%04X', LENGTH(l)) || h || REPEAT('00', GREATEST(0, 4 - LENGTH(l)) * 2))
2066            ELSE UNHEX('08000000000000000000' || h)
2067        END
2068        FROM (
2069            SELECT l, COALESCE(LIST_REDUCE(
2070                LIST_TRANSFORM(l, __x -> PRINTF('%02X%02X', CAST(__x AS INT) & 255, (CAST(__x AS INT) >> 8) & 255)),
2071                (__a, __b) -> __a || __b, ''
2072            ), '') AS h
2073            FROM (SELECT LIST_SORT(LIST_DISTINCT(LIST(:arg) FILTER(NOT :arg IS NULL))) AS l)
2074        )
2075        """
2076    )
2077
2078    # Template for RANDSTR transpilation - placeholders get replaced with actual parameters
2079    RANDSTR_TEMPLATE: exp.Expr = exp.maybe_parse(
2080        f"""
2081        SELECT LISTAGG(
2082            SUBSTRING(
2083                '{RANDSTR_CHAR_POOL}',
2084                1 + CAST(FLOOR(random_value * 62) AS INT),
2085                1
2086            ),
2087            ''
2088        )
2089        FROM (
2090            SELECT (ABS(HASH(i + :seed)) % 1000) / 1000.0 AS random_value
2091            FROM RANGE(:length) AS t(i)
2092        )
2093        """,
2094    )
2095
2096    # Template for MINHASH transpilation
2097    # Computes k minimum hash values across aggregated data using DuckDB list functions
2098    # Returns JSON matching Snowflake format: {"state": [...], "type": "minhash", "version": 1}
2099    MINHASH_TEMPLATE: exp.Expr = exp.maybe_parse(
2100        """
2101        SELECT JSON_OBJECT('state', LIST(min_h ORDER BY seed), 'type', 'minhash', 'version', 1)
2102        FROM (
2103            SELECT seed, LIST_MIN(LIST_TRANSFORM(vals, __v -> HASH(CAST(__v AS VARCHAR) || CAST(seed AS VARCHAR)))) AS min_h
2104            FROM (SELECT LIST(:expr) AS vals), RANGE(0, :k) AS t(seed)
2105        )
2106        """,
2107    )
2108
2109    # Template for MINHASH_COMBINE transpilation
2110    # Combines multiple minhash signatures by taking element-wise minimum
2111    MINHASH_COMBINE_TEMPLATE: exp.Expr = exp.maybe_parse(
2112        """
2113        SELECT JSON_OBJECT('state', LIST(min_h ORDER BY idx), 'type', 'minhash', 'version', 1)
2114        FROM (
2115            SELECT
2116                pos AS idx,
2117                MIN(val) AS min_h
2118            FROM
2119                UNNEST(LIST(:expr)) AS _(sig),
2120                UNNEST(CAST(sig -> 'state' AS UBIGINT[])) WITH ORDINALITY AS t(val, pos)
2121            GROUP BY pos
2122        )
2123        """,
2124    )
2125
2126    # Template for APPROXIMATE_SIMILARITY transpilation
2127    # Computes multi-way Jaccard similarity: fraction of positions where ALL signatures agree
2128    APPROXIMATE_SIMILARITY_TEMPLATE: exp.Expr = exp.maybe_parse(
2129        """
2130        SELECT CAST(SUM(CASE WHEN num_distinct = 1 THEN 1 ELSE 0 END) AS DOUBLE) / COUNT(*)
2131        FROM (
2132            SELECT pos, COUNT(DISTINCT h) AS num_distinct
2133            FROM (
2134                SELECT h, pos
2135                FROM UNNEST(LIST(:expr)) AS _(sig),
2136                     UNNEST(CAST(sig -> 'state' AS UBIGINT[])) WITH ORDINALITY AS s(h, pos)
2137            )
2138            GROUP BY pos
2139        )
2140        """,
2141    )
2142
2143    # Template for ARRAYS_ZIP transpilation
2144    # Snowflake pads to longest array; DuckDB LIST_ZIP truncates to shortest
2145    # Uses RANGE + indexing to match Snowflake behavior
2146    ARRAYS_ZIP_TEMPLATE: exp.Expr = exp.maybe_parse(
2147        """
2148        CASE WHEN :null_check THEN NULL
2149        WHEN :all_empty_check THEN [:empty_struct]
2150        ELSE LIST_TRANSFORM(RANGE(0, :max_len), __i -> :transform_struct)
2151        END
2152        """,
2153    )
2154
2155    UUID_V5_TEMPLATE: exp.Expr = exp.maybe_parse(
2156        """
2157        (SELECT
2158            LOWER(
2159                SUBSTR(h, 1, 8) || '-' ||
2160                SUBSTR(h, 9, 4) || '-' ||
2161                '5' || SUBSTR(h, 14, 3) || '-' ||
2162                FORMAT('{:02x}', CAST('0x' || SUBSTR(h, 17, 2) AS INT) & 63 | 128) || SUBSTR(h, 19, 2) || '-' ||
2163                SUBSTR(h, 21, 12)
2164            )
2165        FROM (
2166            SELECT SUBSTR(SHA1(UNHEX(REPLACE(:namespace, '-', '')) || ENCODE(:name, 'utf8')), 1, 32) AS h
2167        ))
2168        """
2169    )
2170
2171    # Shared bag semantics outer frame for ARRAY_EXCEPT and ARRAY_INTERSECTION.
2172    # Each element is paired with its 1-based position via LIST_ZIP, then filtered
2173    # by a comparison operator (supplied via :cond) that determines the operation:
2174    #   EXCEPT (>):        keep the N-th occurrence only if N > count in arr2
2175    #                      e.g. [2,2,2] EXCEPT [2,2] -> [2]
2176    #   INTERSECTION (<=): keep the N-th occurrence only if N <= count in arr2
2177    #                      e.g. [2,2,2] INTERSECT [2,2] -> [2,2]
2178    # IS NOT DISTINCT FROM is used for NULL-safe element comparison.
2179    ARRAY_BAG_TEMPLATE: exp.Expr = exp.maybe_parse(
2180        """
2181        CASE
2182            WHEN :arr1 IS NULL OR :arr2 IS NULL THEN NULL
2183            ELSE LIST_TRANSFORM(
2184                LIST_FILTER(
2185                    LIST_ZIP(:arr1, GENERATE_SERIES(1, LEN(:arr1))),
2186                    pair -> :cond
2187                ),
2188                pair -> pair[0]
2189            )
2190        END
2191        """
2192    )
2193
2194    ARRAY_EXCEPT_CONDITION: exp.Expr = exp.maybe_parse(
2195        "LEN(LIST_FILTER(:arr1[1:pair[1]], e -> e IS NOT DISTINCT FROM pair[0]))"
2196        " > LEN(LIST_FILTER(:arr2, e -> e IS NOT DISTINCT FROM pair[0]))"
2197    )
2198
2199    ARRAY_INTERSECTION_CONDITION: exp.Expr = exp.maybe_parse(
2200        "LEN(LIST_FILTER(:arr1[1:pair[1]], e -> e IS NOT DISTINCT FROM pair[0]))"
2201        " <= LEN(LIST_FILTER(:arr2, e -> e IS NOT DISTINCT FROM pair[0]))"
2202    )
2203
2204    # Set semantics for ARRAY_EXCEPT. Deduplicates arr1 via LIST_DISTINCT, then
2205    # filters out any element that appears at least once in arr2.
2206    #   e.g. [1,1,2,3] EXCEPT [1] -> [2,3]
2207    # IS NOT DISTINCT FROM is used for NULL-safe element comparison.
2208    ARRAY_EXCEPT_SET_TEMPLATE: exp.Expr = exp.maybe_parse(
2209        """
2210        CASE
2211            WHEN :arr1 IS NULL OR :arr2 IS NULL THEN NULL
2212            ELSE LIST_FILTER(
2213                LIST_DISTINCT(:arr1),
2214                e -> LEN(LIST_FILTER(:arr2, x -> x IS NOT DISTINCT FROM e)) = 0
2215            )
2216        END
2217        """
2218    )
2219
2220    # BigQuery's `x IN UNNEST(arr)` NULL semantics:
2221    #   NULL IN UNNEST([1, 2])  -> NULL
2222    #   3 IN UNNEST([1, NULL])  -> NULL
2223    #   3 IN UNNEST([1, 2])     -> FALSE
2224    #   1 IN UNNEST(NULL)       -> FALSE (not NULL)
2225    #   1 IN UNNEST([])         -> FALSE
2226    # The default `IN (SELECT UNNEST(...))` rewrite creates a correlated subquery
2227    # that DuckDB rejects inside non-inner joins, so a CASE expression is used instead.
2228    IN_UNNEST_TEMPLATE: exp.Expr = exp.maybe_parse(
2229        """
2230        CASE
2231            WHEN :arr IS NULL OR ARRAY_LENGTH(:arr) = 0 THEN FALSE
2232            WHEN ARRAY_CONTAINS(:arr, :value) THEN TRUE
2233            WHEN :value IS NULL OR ARRAY_LENGTH(:arr) <> LIST_COUNT(:arr) THEN NULL
2234            ELSE FALSE
2235        END
2236        """
2237    )
2238
2239    STRTOK_TO_ARRAY_TEMPLATE: exp.Expr = exp.maybe_parse(
2240        """
2241        CASE WHEN :delimiter IS NULL THEN NULL
2242        ELSE LIST_FILTER(
2243            REGEXP_SPLIT_TO_ARRAY(:string, CASE WHEN :delimiter = '' THEN '.^' ELSE CONCAT('[', :escaped, ']') END),
2244            x -> NOT x = ''
2245        ) END
2246        """
2247    )
2248
2249    # Template for STRTOK function transpilation
2250    #
2251    # DuckDB itself doesn't have a strtok function. This handles the transpilation from Snowflake to DuckDB.
2252    # We may need to adjust this if we want to support transpilation from other dialects
2253    #
2254    # CASE
2255    #     -- Snowflake: empty delimiter + empty input string -> NULL
2256    #     WHEN delimiter = '' AND input_str = '' THEN NULL
2257    #
2258    #     -- Snowflake: empty delimiter + non-empty input string -> treats whole input as 1 token -> return input string if index is 1
2259    #     WHEN delimiter = '' AND index = 1 THEN input_str
2260    #
2261    #     -- Snowflake: empty delimiter + non-empty input string -> treats whole input as 1 token -> return NULL if index is not 1
2262    #     WHEN delimiter = '' THEN NULL
2263    #
2264    #     -- Snowflake: negative indices return NULL
2265    #     WHEN index < 0 THEN NULL
2266    #
2267    #     -- Snowflake: return NULL if any argument is NULL
2268    #     WHEN input_str IS NULL OR delimiter IS NULL OR index IS NULL THEN NULL
2269    #
2270    #
2271    #     ELSE LIST_FILTER(
2272    #         REGEXP_SPLIT_TO_ARRAY(
2273    #             input_str,
2274    #             CASE
2275    #                 -- if delimiter is '', we don't want to surround it with '[' and ']' as '[]' is invalid for DuckDB
2276    #                 WHEN delimiter = '' THEN ''
2277    #
2278    #                 -- handle problematic regex characters in delimiter with REGEXP_REPLACE
2279    #                 -- turn delimiter into a regex char set, otherwise DuckDB will match in order, which we don't want
2280    #                 ELSE '[' || REGEXP_REPLACE(delimiter, problematic_char_set, '\\\1', 'g') || ']'
2281    #             END
2282    #         ),
2283    #
2284    #         -- Snowflake: don't return empty strings
2285    #         x -> NOT x = ''
2286    #     )[index]
2287    # END
2288    STRTOK_TEMPLATE: exp.Expr = exp.maybe_parse(
2289        """
2290        CASE
2291            WHEN :delimiter = '' AND :string = '' THEN NULL
2292            WHEN :delimiter = '' AND :part_index = 1 THEN :string
2293            WHEN :delimiter = '' THEN NULL
2294            WHEN :part_index < 0 THEN NULL
2295            WHEN :string IS NULL OR :delimiter IS NULL OR :part_index IS NULL THEN NULL
2296            ELSE :base_func
2297        END
2298        """
2299    )
2300
2301    # Snowflake AUTO detects 3 DATE formats: YYYY-MM-DD (ISO-8601), MM/DD/YYYY, DD-MON-YYYY.
2302    # DuckDB TRY_CAST handles ISO-8601 natively. For the other two formats we use CONTAINS('/')
2303    # and REGEXP_MATCHES('[A-Za-z]') as heuristics — these correctly handle single-digit months
2304    # and days (e.g. 1/5/2020, 5-JAN-2020) where a positional char check would fail.
2305    # Ref: https://docs.snowflake.com/en/sql-reference/date-time-input-output#date-formats
2306    _TRYCAST_DATE_SLASH_FMT = "%m/%d/%Y"
2307    _TRYCAST_DATE_MON_FMT = "%d-%b-%Y"
2308
2309    def _array_bag_sql(self, condition: exp.Expr, arr1: exp.Expr, arr2: exp.Expr) -> str:
2310        cond = exp.Paren(this=exp.replace_placeholders(condition, arr1=arr1, arr2=arr2))
2311        return self.sql(
2312            exp.replace_placeholders(self.ARRAY_BAG_TEMPLATE, arr1=arr1, arr2=arr2, cond=cond)
2313        )
2314
2315    def timeslice_sql(self, expression: exp.TimeSlice) -> str:
2316        """
2317        Transform Snowflake's TIME_SLICE to DuckDB's time_bucket.
2318
2319        Snowflake: TIME_SLICE(date_expr, slice_length, 'UNIT' [, 'START'|'END'])
2320        DuckDB:    time_bucket(INTERVAL 'slice_length' UNIT, date_expr)
2321
2322        For 'END' kind, add the interval to get the end of the slice.
2323        For DATE type with 'END', cast result back to DATE to preserve type.
2324        """
2325        date_expr = expression.this
2326        slice_length = expression.expression
2327        unit = expression.unit
2328        kind = expression.text("kind").upper()
2329
2330        # Create INTERVAL expression: INTERVAL 'N' UNIT
2331        interval_expr = exp.Interval(this=slice_length, unit=unit)
2332
2333        # Create base time_bucket expression
2334        time_bucket_expr = exp.func("time_bucket", interval_expr, date_expr)
2335
2336        # Check if we need the end of the slice (default is start)
2337        if not kind == "END":
2338            # For 'START', return time_bucket directly
2339            return self.sql(time_bucket_expr)
2340
2341        # For 'END', add the interval to get end of slice
2342        add_expr = exp.Add(this=time_bucket_expr, expression=interval_expr.copy())
2343
2344        # If input is DATE type, cast result back to DATE to preserve type
2345        # DuckDB converts DATE to TIMESTAMP when adding intervals
2346        if date_expr.is_type(exp.DType.DATE):
2347            return self.sql(exp.cast(add_expr, exp.DType.DATE))
2348
2349        return self.sql(add_expr)
2350
2351    def bitmapbucketnumber_sql(self, expression: exp.BitmapBucketNumber) -> str:
2352        """
2353        Transpile BITMAP_BUCKET_NUMBER function from Snowflake to DuckDB equivalent.
2354
2355        Snowflake's BITMAP_BUCKET_NUMBER returns a 1-based bucket identifier where:
2356        - Each bucket covers 32,768 values
2357        - Bucket numbering starts at 1
2358        - Formula: ((value - 1) // 32768) + 1 for positive values
2359
2360        For non-positive values (0 and negative), we use value // 32768 to avoid
2361        producing bucket 0 or positive bucket IDs for negative inputs.
2362        """
2363        value = expression.this
2364
2365        positive_formula = ((value - 1) // 32768) + 1
2366        non_positive_formula = value // 32768
2367
2368        # CASE WHEN value > 0 THEN ((value - 1) // 32768) + 1 ELSE value // 32768 END
2369        case_expr = (
2370            exp.case()
2371            .when(exp.GT(this=value, expression=exp.Literal.number(0)), positive_formula)
2372            .else_(non_positive_formula)
2373        )
2374        return self.sql(case_expr)
2375
2376    def bitmapbitposition_sql(self, expression: exp.BitmapBitPosition) -> str:
2377        """
2378        Transpile Snowflake's BITMAP_BIT_POSITION to DuckDB CASE expression.
2379
2380        Snowflake's BITMAP_BIT_POSITION behavior:
2381        - For n <= 0: returns ABS(n) % 32768
2382        - For n > 0: returns (n - 1) % 32768 (maximum return value is 32767)
2383        """
2384        this = expression.this
2385
2386        return self.sql(
2387            exp.Mod(
2388                this=exp.Paren(
2389                    this=exp.If(
2390                        this=exp.GT(this=this, expression=exp.Literal.number(0)),
2391                        true=this - exp.Literal.number(1),
2392                        false=exp.Abs(this=this),
2393                    )
2394                ),
2395                expression=MAX_BIT_POSITION,
2396            )
2397        )
2398
2399    def bitmapconstructagg_sql(self, expression: exp.BitmapConstructAgg) -> str:
2400        """
2401        Transpile Snowflake's BITMAP_CONSTRUCT_AGG to DuckDB equivalent.
2402        Uses a pre-parsed template with placeholders replaced by expression nodes.
2403
2404        Snowflake bitmap format:
2405        - Small (< 5 unique values): 2-byte count (big-endian) + values (little-endian) + padding to 10 bytes
2406        - Large (>= 5 unique values): 10-byte header (0x08 + 9 zeros) + values (little-endian)
2407        """
2408        arg = expression.this
2409        return (
2410            f"({self.sql(exp.replace_placeholders(self.BITMAP_CONSTRUCT_AGG_TEMPLATE, arg=arg))})"
2411        )
2412
2413    def getignorecase_sql(self, expression: exp.GetIgnoreCase) -> str:
2414        self.unsupported("DuckDB does not support the GET_IGNORE_CASE() function")
2415        return self.function_fallback_sql(expression)
2416
2417    def compress_sql(self, expression: exp.Compress) -> str:
2418        self.unsupported("DuckDB does not support the COMPRESS() function")
2419        return self.function_fallback_sql(expression)
2420
2421    def encrypt_sql(self, expression: exp.Encrypt) -> str:
2422        self.unsupported("ENCRYPT is not supported in DuckDB")
2423        return self.function_fallback_sql(expression)
2424
2425    def decrypt_sql(self, expression: exp.Decrypt) -> str:
2426        func_name = "TRY_DECRYPT" if expression.args.get("safe") else "DECRYPT"
2427        self.unsupported(f"{func_name} is not supported in DuckDB")
2428        return self.function_fallback_sql(expression)
2429
2430    def decryptraw_sql(self, expression: exp.DecryptRaw) -> str:
2431        func_name = "TRY_DECRYPT_RAW" if expression.args.get("safe") else "DECRYPT_RAW"
2432        self.unsupported(f"{func_name} is not supported in DuckDB")
2433        return self.function_fallback_sql(expression)
2434
2435    def encryptraw_sql(self, expression: exp.EncryptRaw) -> str:
2436        self.unsupported("ENCRYPT_RAW is not supported in DuckDB")
2437        return self.function_fallback_sql(expression)
2438
2439    def parseurl_sql(self, expression: exp.ParseUrl) -> str:
2440        self.unsupported("PARSE_URL is not supported in DuckDB")
2441        return self.function_fallback_sql(expression)
2442
2443    def parseip_sql(self, expression: exp.ParseIp) -> str:
2444        self.unsupported("PARSE_IP is not supported in DuckDB")
2445        return self.function_fallback_sql(expression)
2446
2447    def decompressstring_sql(self, expression: exp.DecompressString) -> str:
2448        self.unsupported("DECOMPRESS_STRING is not supported in DuckDB")
2449        return self.function_fallback_sql(expression)
2450
2451    def decompressbinary_sql(self, expression: exp.DecompressBinary) -> str:
2452        self.unsupported("DECOMPRESS_BINARY is not supported in DuckDB")
2453        return self.function_fallback_sql(expression)
2454
2455    def jarowinklersimilarity_sql(self, expression: exp.JarowinklerSimilarity) -> str:
2456        this = expression.this
2457        expr = expression.expression
2458
2459        if expression.args.get("case_insensitive"):
2460            this = exp.Upper(this=this)
2461            expr = exp.Upper(this=expr)
2462
2463        result = exp.func("JARO_WINKLER_SIMILARITY", this, expr)
2464
2465        if expression.args.get("integer_scale"):
2466            result = exp.cast(result * 100, "INTEGER")
2467
2468        return self.sql(result)
2469
2470    def nthvalue_sql(self, expression: exp.NthValue) -> str:
2471        from_first = expression.args.get("from_first", True)
2472        if not from_first:
2473            self.unsupported("DuckDB's NTH_VALUE doesn't support starting from the end ")
2474
2475        return self.function_fallback_sql(expression)
2476
2477    def randstr_sql(self, expression: exp.Randstr) -> str:
2478        """
2479        Transpile Snowflake's RANDSTR to DuckDB equivalent using deterministic hash-based random.
2480        Uses a pre-parsed template with placeholders replaced by expression nodes.
2481
2482        RANDSTR(length, generator) generates a random string of specified length.
2483        - With numeric seed: Use HASH(i + seed) for deterministic output (same seed = same result)
2484        - With RANDOM(): Use RANDOM() in the hash for non-deterministic output
2485        - No generator: Use default seed value
2486        """
2487        length = expression.this
2488        generator = expression.args.get("generator")
2489
2490        if generator:
2491            if isinstance(generator, exp.Rand):
2492                # If it's RANDOM(), use its seed if available, otherwise use RANDOM() itself
2493                seed_value = generator.this or generator
2494            else:
2495                # Const/int or other expression - use as seed directly
2496                seed_value = generator
2497        else:
2498            # No generator specified, use default seed (arbitrary but deterministic)
2499            seed_value = exp.Literal.number(RANDSTR_SEED)
2500
2501        replacements = {"seed": seed_value, "length": length}
2502        return f"({self.sql(exp.replace_placeholders(self.RANDSTR_TEMPLATE, **replacements))})"
2503
2504    @unsupported_args("finish")
2505    def reduce_sql(self, expression: exp.Reduce) -> str:
2506        array_arg = expression.this
2507        initial_value = expression.args.get("initial")
2508        merge_lambda = expression.args.get("merge")
2509
2510        if merge_lambda:
2511            merge_lambda.set("colon", True)
2512
2513        return self.func("list_reduce", array_arg, merge_lambda, initial_value)
2514
2515    def zipf_sql(self, expression: exp.Zipf) -> str:
2516        """
2517        Transpile Snowflake's ZIPF to DuckDB using CDF-based inverse sampling.
2518        Uses a pre-parsed template with placeholders replaced by expression nodes.
2519        """
2520        s = expression.this
2521        n = expression.args["elementcount"]
2522        gen = expression.args["gen"]
2523
2524        if not isinstance(gen, exp.Rand):
2525            # (ABS(HASH(seed)) % 1000000) / 1000000.0
2526            random_expr: exp.Expr = exp.Div(
2527                this=exp.Paren(
2528                    this=exp.Mod(
2529                        this=exp.Abs(this=exp.Anonymous(this="HASH", expressions=[gen.copy()])),
2530                        expression=exp.Literal.number(1000000),
2531                    )
2532                ),
2533                expression=exp.Literal.number(1000000.0),
2534            )
2535        else:
2536            # Use RANDOM() for non-deterministic output
2537            random_expr = exp.Rand()
2538
2539        replacements = {"s": s, "n": n, "random_expr": random_expr}
2540        return f"({self.sql(exp.replace_placeholders(self.ZIPF_TEMPLATE, **replacements))})"
2541
2542    def tobinary_sql(self, expression: exp.ToBinary) -> str:
2543        """
2544        TO_BINARY and TRY_TO_BINARY transpilation:
2545        - 'HEX': TO_BINARY('48454C50', 'HEX') -> UNHEX('48454C50')
2546        - 'UTF-8': TO_BINARY('TEST', 'UTF-8') -> ENCODE('TEST')
2547        - 'BASE64': TO_BINARY('SEVMUA==', 'BASE64') -> FROM_BASE64('SEVMUA==')
2548
2549        For TRY_TO_BINARY (safe=True), wrap with TRY():
2550        - 'HEX': TRY_TO_BINARY('invalid', 'HEX') -> TRY(UNHEX('invalid'))
2551        """
2552        value = expression.this
2553        format_arg = expression.args.get("format")
2554        is_safe = expression.args.get("safe")
2555        is_binary = _is_binary(expression)
2556
2557        if not format_arg and not is_binary:
2558            func_name = "TRY_TO_BINARY" if is_safe else "TO_BINARY"
2559            return self.func(func_name, value)
2560
2561        # Snowflake defaults to HEX encoding when no format is specified
2562        fmt = format_arg.name.upper() if format_arg else "HEX"
2563
2564        if fmt in ("UTF-8", "UTF8"):
2565            # DuckDB ENCODE always uses UTF-8, no charset parameter needed
2566            result = self.func("ENCODE", value)
2567        elif fmt == "BASE64":
2568            result = self.func("FROM_BASE64", value)
2569        elif fmt == "HEX":
2570            result = self.func("UNHEX", value)
2571        else:
2572            if is_safe:
2573                return self.sql(exp.null())
2574            else:
2575                self.unsupported(f"format {fmt} is not supported")
2576                result = self.func("TO_BINARY", value)
2577        return f"TRY({result})" if is_safe else result
2578
2579    def tonumber_sql(self, expression: exp.ToNumber) -> str:
2580        fmt = expression.args.get("format")
2581        precision = expression.args.get("precision")
2582        scale = expression.args.get("scale")
2583
2584        if not fmt and precision and scale:
2585            return self.sql(
2586                exp.cast(
2587                    expression.this, f"DECIMAL({precision.name}, {scale.name})", dialect="duckdb"
2588                )
2589            )
2590
2591        return super().tonumber_sql(expression)
2592
2593    def _greatest_least_sql(self, expression: exp.Greatest | exp.Least) -> str:
2594        """
2595        Handle GREATEST/LEAST functions with dialect-aware NULL behavior.
2596
2597        - If ignore_nulls=False (BigQuery-style): return NULL if any argument is NULL
2598        - If ignore_nulls=True (DuckDB/PostgreSQL-style): ignore NULLs, return greatest/least non-NULL value
2599        """
2600        # Get all arguments
2601        all_args = [expression.this, *expression.expressions]
2602        fallback_sql = self.function_fallback_sql(expression)
2603
2604        if expression.args.get("ignore_nulls"):
2605            # DuckDB/PostgreSQL behavior: use native GREATEST/LEAST (ignores NULLs)
2606            return self.sql(fallback_sql)
2607
2608        # return NULL if any argument is NULL
2609        case_expr = exp.case().when(
2610            exp.or_(*[arg.is_(exp.null()) for arg in all_args], copy=False),
2611            exp.null(),
2612            copy=False,
2613        )
2614        case_expr.set("default", fallback_sql)
2615        return self.sql(case_expr)
2616
2617    def generator_sql(self, expression: exp.Generator) -> str:
2618        # Transpile Snowflake GENERATOR to DuckDB range()
2619        rowcount = expression.args.get("rowcount")
2620        time_limit = expression.args.get("time_limit")
2621
2622        if time_limit:
2623            self.unsupported("GENERATOR TIMELIMIT parameter is not supported in DuckDB")
2624
2625        if not rowcount:
2626            self.unsupported("GENERATOR without ROWCOUNT is not supported in DuckDB")
2627            return self.func("range", exp.Literal.number(0))
2628
2629        return self.func("range", rowcount)
2630
2631    def greatest_sql(self, expression: exp.Greatest) -> str:
2632        return self._greatest_least_sql(expression)
2633
2634    def least_sql(self, expression: exp.Least) -> str:
2635        return self._greatest_least_sql(expression)
2636
2637    def lambda_sql(self, expression: exp.Lambda, arrow_sep: str = "->", wrap: bool = True) -> str:
2638        if expression.args.get("colon"):
2639            prefix = "LAMBDA "
2640            arrow_sep = ":"
2641            wrap = False
2642        else:
2643            prefix = ""
2644
2645        lambda_sql = super().lambda_sql(expression, arrow_sep=arrow_sep, wrap=wrap)
2646        return f"{prefix}{lambda_sql}"
2647
2648    def show_sql(self, expression: exp.Show) -> str:
2649        from_ = self.sql(expression, "from_")
2650        from_ = f" FROM {from_}" if from_ else ""
2651        return f"SHOW {expression.name}{from_}"
2652
2653    def soundex_sql(self, expression: exp.Soundex) -> str:
2654        self.unsupported("SOUNDEX is not supported in DuckDB")
2655        return self.func("SOUNDEX", expression.this)
2656
2657    def sortarray_sql(self, expression: exp.SortArray) -> str:
2658        arr = expression.this
2659        asc = expression.args.get("asc")
2660        nulls_first = expression.args.get("nulls_first")
2661
2662        if not isinstance(asc, exp.Boolean) and not isinstance(nulls_first, exp.Boolean):
2663            return self.func("LIST_SORT", arr, asc, nulls_first)
2664
2665        nulls_are_first = nulls_first == exp.true()
2666        nulls_first_sql = exp.Literal.string("NULLS FIRST") if nulls_are_first else None
2667
2668        if not isinstance(asc, exp.Boolean):
2669            return self.func("LIST_SORT", arr, asc, nulls_first_sql)
2670
2671        descending = asc == exp.false()
2672
2673        if not descending and not nulls_are_first:
2674            return self.func("LIST_SORT", arr)
2675        if not nulls_are_first:
2676            return self.func("ARRAY_REVERSE_SORT", arr)
2677        return self.func(
2678            "LIST_SORT",
2679            arr,
2680            exp.Literal.string("DESC" if descending else "ASC"),
2681            exp.Literal.string("NULLS FIRST"),
2682        )
2683
2684    def install_sql(self, expression: exp.Install) -> str:
2685        force = "FORCE " if expression.args.get("force") else ""
2686        this = self.sql(expression, "this")
2687        from_clause = expression.args.get("from_")
2688        from_clause = f" FROM {from_clause}" if from_clause else ""
2689        return f"{force}INSTALL {this}{from_clause}"
2690
2691    def approxtopk_sql(self, expression: exp.ApproxTopK) -> str:
2692        self.unsupported(
2693            "APPROX_TOP_K cannot be transpiled to DuckDB due to incompatible return types. "
2694        )
2695        return self.function_fallback_sql(expression)
2696
2697    def strposition_sql(self, expression: exp.StrPosition) -> str:
2698        this = expression.this
2699        substr = expression.args.get("substr")
2700        position = expression.args.get("position")
2701
2702        # For BINARY/BLOB: DuckDB's STRPOS doesn't support BLOB types
2703        # Convert to HEX strings, use STRPOS, then convert hex position to byte position
2704        if _is_binary(this):
2705            # Build expression: STRPOS(HEX(haystack), HEX(needle))
2706            hex_strpos = exp.StrPosition(
2707                this=exp.Hex(this=this),
2708                substr=exp.Hex(this=substr),
2709            )
2710
2711            return self.sql(exp.cast((hex_strpos + 1) / 2, exp.DType.INT))
2712
2713        # For VARCHAR: handle clamp_position
2714        if expression.args.get("clamp_position") and position:
2715            expression = expression.copy()
2716            expression.set(
2717                "position",
2718                exp.If(
2719                    this=exp.LTE(this=position, expression=exp.Literal.number(0)),
2720                    true=exp.Literal.number(1),
2721                    false=position.copy(),
2722                ),
2723            )
2724
2725        return strposition_sql(self, expression)
2726
2727    def substring_sql(self, expression: exp.Substring) -> str:
2728        if expression.args.get("zero_start"):
2729            start = expression.args.get("start")
2730            length = expression.args.get("length")
2731
2732            if start := expression.args.get("start"):
2733                start = exp.If(this=start.eq(0), true=exp.Literal.number(1), false=start)
2734            if length := expression.args.get("length"):
2735                length = exp.If(this=length < 0, true=exp.Literal.number(0), false=length)
2736
2737            return self.func("SUBSTRING", expression.this, start, length)
2738
2739        return self.function_fallback_sql(expression)
2740
2741    def strtotime_sql(self, expression: exp.StrToTime) -> str:
2742        # Check if target_type requires TIMESTAMPTZ (for LTZ/TZ variants)
2743        target_type = expression.args.get("target_type")
2744        needs_tz = target_type and target_type.this in (
2745            exp.DType.TIMESTAMPLTZ,
2746            exp.DType.TIMESTAMPTZ,
2747        )
2748
2749        value, formatted_time = self._strptime_default_year(expression)
2750
2751        if expression.args.get("safe"):
2752            cast_type = exp.DType.TIMESTAMPTZ if needs_tz else exp.DType.TIMESTAMP
2753            return self.sql(exp.cast(self.func("TRY_STRPTIME", value, formatted_time), cast_type))
2754
2755        base_sql = self.func("STRPTIME", value, formatted_time)
2756        if needs_tz:
2757            return self.sql(
2758                exp.cast(
2759                    base_sql,
2760                    exp.DataType(this=exp.DType.TIMESTAMPTZ),
2761                )
2762            )
2763        return base_sql
2764
2765    def strtodate_sql(self, expression: exp.StrToDate) -> str:
2766        value, formatted_time = self._strptime_default_year(expression)
2767        function_name = "STRPTIME" if not expression.args.get("safe") else "TRY_STRPTIME"
2768        return self.sql(
2769            exp.cast(
2770                self.func(function_name, value, formatted_time),
2771                exp.DataType(this=exp.DType.DATE),
2772            )
2773        )
2774
2775    def _strptime_default_year(
2776        self, expression: exp.StrToTime | exp.StrToDate | exp.ParseDatetime
2777    ) -> tuple[exp.ExpOrStr, exp.ExpOrStr | None]:
2778        value: exp.ExpOrStr = expression.this
2779        formatted_time: exp.ExpOrStr | None = self.format_time(expression)
2780
2781        if default_year := expression.args.get("default_year"):
2782            value = exp.DPipe(this=exp.Literal.string(f"{default_year.name} "), expression=value)
2783            formatted_time = exp.DPipe(this=exp.Literal.string("%Y "), expression=formatted_time)
2784
2785        return value, formatted_time
2786
2787    def parsedatetime_sql(self, expression: exp.ParseDatetime) -> str:
2788        value, formatted_time = self._strptime_default_year(expression)
2789        return self.func("STRPTIME", value, formatted_time)
2790
2791    def parsetime_sql(self, expression: exp.ParseTime) -> str:
2792        formatted_time = self.format_time(expression)
2793        return self.sql(
2794            exp.cast(
2795                self.func("STRPTIME", expression.this, formatted_time),
2796                exp.DataType(this=exp.DType.TIME),
2797            )
2798        )
2799
2800    def tsordstotime_sql(self, expression: exp.TsOrDsToTime) -> str:
2801        this = expression.this
2802        time_format = self.format_time(expression)
2803        safe = expression.args.get("safe")
2804        time_type = exp.DataType.from_str("TIME", dialect="duckdb")
2805        cast_expr = exp.TryCast if safe else exp.Cast
2806
2807        if time_format:
2808            func_name = "TRY_STRPTIME" if safe else "STRPTIME"
2809            strptime = exp.Anonymous(this=func_name, expressions=[this, time_format])
2810            return self.sql(cast_expr(this=strptime, to=time_type))
2811
2812        if isinstance(this, exp.TsOrDsToTime) or this.is_type(exp.DType.TIME):
2813            return self.sql(this)
2814
2815        return self.sql(cast_expr(this=this, to=time_type))
2816
2817    def currentdate_sql(self, expression: exp.CurrentDate) -> str:
2818        if not expression.this:
2819            return "CURRENT_DATE"
2820
2821        expr = exp.Cast(
2822            this=exp.AtTimeZone(this=exp.CurrentTimestamp(), zone=expression.this),
2823            to=exp.DataType(this=exp.DType.DATE),
2824        )
2825        return self.sql(expr)
2826
2827    def checkjson_sql(self, expression: exp.CheckJson) -> str:
2828        arg = expression.this
2829        return self.sql(
2830            exp.case()
2831            .when(
2832                exp.or_(arg.is_(exp.Null()), arg.eq(""), exp.func("json_valid", arg)),
2833                exp.null(),
2834            )
2835            .else_(exp.Literal.string("Invalid JSON"))
2836        )
2837
2838    def parsejson_sql(self, expression: exp.ParseJSON) -> str:
2839        arg = expression.this
2840        if expression.args.get("safe"):
2841            return self.sql(
2842                exp.case()
2843                .when(exp.func("json_valid", arg), exp.cast(arg.copy(), "JSON"))
2844                .else_(exp.null())
2845            )
2846        return self.func("JSON", arg)
2847
2848    def unicode_sql(self, expression: exp.Unicode) -> str:
2849        if expression.args.get("empty_is_zero"):
2850            return self.sql(
2851                exp.case()
2852                .when(expression.this.eq(exp.Literal.string("")), exp.Literal.number(0))
2853                .else_(exp.Anonymous(this="UNICODE", expressions=[expression.this]))
2854            )
2855
2856        return self.func("UNICODE", expression.this)
2857
2858    def stripnullvalue_sql(self, expression: exp.StripNullValue) -> str:
2859        return self.sql(
2860            exp.case()
2861            .when(exp.func("json_type", expression.this).eq("NULL"), exp.null())
2862            .else_(expression.this)
2863        )
2864
2865    def trunc_sql(self, expression: exp.Trunc) -> str:
2866        decimals = expression.args.get("decimals")
2867        if (
2868            expression.args.get("fractions_supported")
2869            and decimals
2870            and not decimals.is_type(exp.DType.INT)
2871        ):
2872            decimals = exp.cast(decimals, exp.DType.INT, dialect="duckdb")
2873
2874        return self.func("TRUNC", expression.this, decimals)
2875
2876    def normal_sql(self, expression: exp.Normal) -> str:
2877        """
2878        Transpile Snowflake's NORMAL(mean, stddev, gen) to DuckDB.
2879
2880        Uses the Box-Muller transform via NORMAL_TEMPLATE.
2881        """
2882        mean = expression.this
2883        stddev = expression.args["stddev"]
2884        gen: exp.Expr = expression.args["gen"]
2885
2886        # Build two uniform random values [0, 1) for Box-Muller transform
2887        if isinstance(gen, exp.Rand) and gen.this is None:
2888            u1: exp.Expr = exp.Rand()
2889            u2: exp.Expr = exp.Rand()
2890        else:
2891            # Seeded: derive two values using HASH with different inputs
2892            seed = gen.this if isinstance(gen, exp.Rand) else gen
2893            u1 = exp.replace_placeholders(self.SEEDED_RANDOM_TEMPLATE, seed=seed)
2894            u2 = exp.replace_placeholders(
2895                self.SEEDED_RANDOM_TEMPLATE,
2896                seed=exp.Add(this=seed.copy(), expression=exp.Literal.number(1)),
2897            )
2898
2899        replacements = {"mean": mean, "stddev": stddev, "u1": u1, "u2": u2}
2900        return self.sql(exp.replace_placeholders(self.NORMAL_TEMPLATE, **replacements))
2901
2902    def uniform_sql(self, expression: exp.Uniform) -> str:
2903        """
2904        Transpile Snowflake's UNIFORM(min, max, gen) to DuckDB.
2905
2906        UNIFORM returns a random value in [min, max]:
2907        - Integer result if both min and max are integers
2908        - Float result if either min or max is a float
2909        """
2910        min_val = expression.this
2911        max_val = expression.expression
2912        gen = expression.args.get("gen")
2913
2914        # Determine if result should be integer (both bounds are integers).
2915        # We do this to emulate Snowflake's behavior, INT -> INT, FLOAT -> FLOAT
2916        is_int_result = min_val.is_int and max_val.is_int
2917
2918        # Build the random value expression [0, 1)
2919        if not isinstance(gen, exp.Rand):
2920            # Seed value: (ABS(HASH(seed)) % 1000000) / 1000000.0
2921            random_expr: exp.Expr = exp.Div(
2922                this=exp.Paren(
2923                    this=exp.Mod(
2924                        this=exp.Abs(this=exp.Anonymous(this="HASH", expressions=[gen])),
2925                        expression=exp.Literal.number(1000000),
2926                    )
2927                ),
2928                expression=exp.Literal.number(1000000.0),
2929            )
2930        else:
2931            random_expr = exp.Rand()
2932
2933        # Build: min + random * (max - min [+ 1 for int])
2934        range_expr: exp.Expr = exp.Sub(this=max_val, expression=min_val)
2935        if is_int_result:
2936            range_expr = exp.Add(this=range_expr, expression=exp.Literal.number(1))
2937
2938        result: exp.Expr = exp.Add(
2939            this=min_val,
2940            expression=exp.Mul(this=random_expr, expression=exp.Paren(this=range_expr)),
2941        )
2942
2943        if is_int_result:
2944            result = exp.Cast(this=exp.Floor(this=result), to=exp.DType.BIGINT.into_expr())
2945
2946        return self.sql(result)
2947
2948    def timefromparts_sql(self, expression: exp.TimeFromParts) -> str:
2949        nano = expression.args.get("nano")
2950        overflow = expression.args.get("overflow")
2951
2952        # Snowflake's TIME_FROM_PARTS supports overflow
2953        if overflow:
2954            hour = expression.args["hour"]
2955            minute = expression.args["min"]
2956            sec = expression.args["sec"]
2957
2958            # Check if values are within normal ranges - use MAKE_TIME for efficiency
2959            if not nano and all(arg.is_int for arg in [hour, minute, sec]):
2960                try:
2961                    h_val = hour.to_py()
2962                    m_val = minute.to_py()
2963                    s_val = sec.to_py()
2964                    if 0 <= h_val <= 23 and 0 <= m_val <= 59 and 0 <= s_val <= 59:
2965                        return rename_func("MAKE_TIME")(self, expression)
2966                except ValueError:
2967                    pass
2968
2969            # Overflow or nanoseconds detected - use INTERVAL arithmetic
2970            if nano:
2971                sec = sec + nano.pop() / exp.Literal.number(1000000000.0)
2972
2973            total_seconds = hour * exp.Literal.number(3600) + minute * exp.Literal.number(60) + sec
2974
2975            return self.sql(
2976                exp.Add(
2977                    this=exp.Cast(
2978                        this=exp.Literal.string("00:00:00"), to=exp.DType.TIME.into_expr()
2979                    ),
2980                    expression=exp.Interval(this=total_seconds, unit=exp.var("SECOND")),
2981                )
2982            )
2983
2984        # Default: MAKE_TIME
2985        if nano:
2986            expression.set(
2987                "sec", expression.args["sec"] + nano.pop() / exp.Literal.number(1000000000.0)
2988            )
2989
2990        return rename_func("MAKE_TIME")(self, expression)
2991
2992    def extract_sql(self, expression: exp.Extract) -> str:
2993        """
2994        Transpile EXTRACT/DATE_PART for DuckDB, handling specifiers not natively supported.
2995
2996        DuckDB doesn't support: WEEKISO, YEAROFWEEK, YEAROFWEEKISO, NANOSECOND,
2997        EPOCH_SECOND (as integer), EPOCH_MILLISECOND, EPOCH_MICROSECOND, EPOCH_NANOSECOND
2998        """
2999        this = expression.this
3000        datetime_expr = expression.expression
3001
3002        # TIMESTAMPTZ extractions may produce different results between Snowflake and DuckDB
3003        # because Snowflake applies server timezone while DuckDB uses local timezone
3004        if datetime_expr.is_type(exp.DType.TIMESTAMPTZ, exp.DType.TIMESTAMPLTZ):
3005            self.unsupported(
3006                "EXTRACT from TIMESTAMPTZ / TIMESTAMPLTZ may produce different results due to timezone handling differences"
3007            )
3008
3009        part_name = this.name.upper()
3010
3011        if part_name in self.EXTRACT_STRFTIME_MAPPINGS:
3012            fmt, cast_type = self.EXTRACT_STRFTIME_MAPPINGS[part_name]
3013
3014            # Problem: strftime doesn't accept TIME and there's no NANOSECOND function
3015            # So, for NANOSECOND with TIME, fallback to MICROSECOND * 1000
3016            is_nano_time = part_name == "NANOSECOND" and datetime_expr.is_type(
3017                exp.DType.TIME, exp.DType.TIMETZ
3018            )
3019
3020            if is_nano_time:
3021                self.unsupported("Parameter NANOSECOND is not supported with TIME type in DuckDB")
3022                return self.sql(
3023                    exp.cast(
3024                        exp.Mul(
3025                            this=exp.Extract(this=exp.var("MICROSECOND"), expression=datetime_expr),
3026                            expression=exp.Literal.number(1000),
3027                        ),
3028                        exp.DataType.from_str(cast_type, dialect="duckdb"),
3029                    )
3030                )
3031
3032            # For NANOSECOND, cast to TIMESTAMP_NS to preserve nanosecond precision
3033            strftime_input = datetime_expr
3034            if part_name == "NANOSECOND":
3035                strftime_input = exp.cast(datetime_expr, exp.DType.TIMESTAMP_NS)
3036
3037            return self.sql(
3038                exp.cast(
3039                    exp.Anonymous(
3040                        this="STRFTIME",
3041                        expressions=[strftime_input, exp.Literal.string(fmt)],
3042                    ),
3043                    exp.DataType.from_str(cast_type, dialect="duckdb"),
3044                )
3045            )
3046
3047        if part_name in self.EXTRACT_EPOCH_MAPPINGS:
3048            func_name = self.EXTRACT_EPOCH_MAPPINGS[part_name]
3049            result: exp.Expr = exp.Anonymous(this=func_name, expressions=[datetime_expr])
3050            # EPOCH returns float, cast to BIGINT for integer result
3051            if part_name == "EPOCH_SECOND":
3052                result = exp.cast(result, exp.DataType.from_str("BIGINT", dialect="duckdb"))
3053            return self.sql(result)
3054
3055        return super().extract_sql(expression)
3056
3057    def timestampfromparts_sql(self, expression: exp.TimestampFromParts) -> str:
3058        # Check if this is the date/time expression form: TIMESTAMP_FROM_PARTS(date_expr, time_expr)
3059        date_expr = expression.this
3060        time_expr = expression.expression
3061
3062        if date_expr is not None and time_expr is not None:
3063            # In DuckDB, DATE + TIME produces TIMESTAMP
3064            return self.sql(exp.Add(this=date_expr, expression=time_expr))
3065
3066        # Component-based form: TIMESTAMP_FROM_PARTS(year, month, day, hour, minute, second, ...)
3067        sec = expression.args.get("sec")
3068        if sec is None:
3069            # This shouldn't happen with valid input, but handle gracefully
3070            return rename_func("MAKE_TIMESTAMP")(self, expression)
3071
3072        milli = expression.args.get("milli")
3073        if milli is not None:
3074            sec += milli.pop() / exp.Literal.number(1000.0)
3075
3076        nano = expression.args.get("nano")
3077        if nano is not None:
3078            sec += nano.pop() / exp.Literal.number(1000000000.0)
3079
3080        if milli or nano:
3081            expression.set("sec", sec)
3082
3083        return rename_func("MAKE_TIMESTAMP")(self, expression)
3084
3085    @unsupported_args("nano")
3086    def timestampltzfromparts_sql(self, expression: exp.TimestampLtzFromParts) -> str:
3087        # Pop nano so rename_func only passes args that MAKE_TIMESTAMP accepts
3088        if nano := expression.args.get("nano"):
3089            nano.pop()
3090
3091        timestamp = rename_func("MAKE_TIMESTAMP")(self, expression)
3092        return f"CAST({timestamp} AS TIMESTAMPTZ)"
3093
3094    @unsupported_args("nano")
3095    def timestamptzfromparts_sql(self, expression: exp.TimestampTzFromParts) -> str:
3096        # Extract zone before popping
3097        zone = expression.args.get("zone")
3098        # Pop zone and nano so rename_func only passes args that MAKE_TIMESTAMP accepts
3099        if zone:
3100            zone = zone.pop()
3101
3102        if nano := expression.args.get("nano"):
3103            nano.pop()
3104
3105        timestamp = rename_func("MAKE_TIMESTAMP")(self, expression)
3106
3107        if zone:
3108            # Use AT TIME ZONE to apply the explicit timezone
3109            return f"{timestamp} AT TIME ZONE {self.sql(zone)}"
3110
3111        return timestamp
3112
3113    def tablesample_sql(
3114        self,
3115        expression: exp.TableSample,
3116        tablesample_keyword: str | None = None,
3117    ) -> str:
3118        if not isinstance(expression.parent, exp.Select):
3119            # This sample clause only applies to a single source, not the entire resulting relation
3120            tablesample_keyword = "TABLESAMPLE"
3121
3122        if expression.args.get("size"):
3123            method = expression.args.get("method")
3124            if method and method.name.upper() != "RESERVOIR":
3125                self.unsupported(
3126                    f"Sampling method {method} is not supported with a discrete sample count, "
3127                    "defaulting to reservoir sampling"
3128                )
3129                expression.set("method", exp.var("RESERVOIR"))
3130
3131        return super().tablesample_sql(expression, tablesample_keyword=tablesample_keyword)
3132
3133    def in_sql(self, expression: exp.In) -> str:
3134        unnest = expression.args.get("unnest")
3135        if unnest:
3136            return self.sql(
3137                exp.replace_placeholders(
3138                    self.IN_UNNEST_TEMPLATE, arr=unnest.expressions[0], value=expression.this
3139                )
3140            )
3141        return super().in_sql(expression)
3142
3143    def join_sql(self, expression: exp.Join) -> str:
3144        if (
3145            not expression.args.get("using")
3146            and not expression.args.get("on")
3147            and not expression.method
3148            and (expression.kind in ("", "INNER", "OUTER"))
3149        ):
3150            # Some dialects support `LEFT/INNER JOIN UNNEST(...)` without an explicit ON clause
3151            # DuckDB doesn't, but we can just add a dummy ON clause that is always true
3152            if isinstance(expression.this, exp.Unnest):
3153                return super().join_sql(expression.on(exp.true()))
3154
3155            expression.set("side", None)
3156            expression.set("kind", None)
3157
3158        return super().join_sql(expression)
3159
3160    def countif_sql(self, expression: exp.CountIf) -> str:
3161        if self.dialect.version >= (1, 2):
3162            this = expression.this
3163            if expression.args.get("zero_on_all_null") and not isinstance(this, exp.Distinct):
3164                # DuckDB >= 1.2's COUNT_IF returns NULL when the condition is NULL on all rows,
3165                # so we wrap the condition in IS TRUE to preserve count-like semantics
3166                expression = exp.CountIf(this=exp.paren(this).is_(exp.true()))
3167            return self.function_fallback_sql(expression)
3168
3169        # https://github.com/tobymao/sqlglot/pull/4749
3170        return count_if_to_sum(self, expression)
3171
3172    def bracket_sql(self, expression: exp.Bracket) -> str:
3173        if self.dialect.version >= (1, 2):
3174            return super().bracket_sql(expression)
3175
3176        # https://duckdb.org/2025/02/05/announcing-duckdb-120.html#breaking-changes
3177        this = expression.this
3178        if isinstance(this, exp.Array):
3179            this.replace(exp.paren(this))
3180
3181        bracket = super().bracket_sql(expression)
3182
3183        if not expression.args.get("returns_list_for_maps"):
3184            if not this.type:
3185                from sqlglot.optimizer.annotate_types import annotate_types
3186
3187                this = annotate_types(this, dialect=self.dialect)
3188
3189            if this.is_type(exp.DType.MAP):
3190                bracket = f"({bracket})[1]"
3191
3192        return bracket
3193
3194    def withingroup_sql(self, expression: exp.WithinGroup) -> str:
3195        func = expression.this
3196
3197        # For ARRAY_AGG, DuckDB requires ORDER BY inside the function, not in WITHIN GROUP
3198        # Transform: ARRAY_AGG(x) WITHIN GROUP (ORDER BY y) -> ARRAY_AGG(x ORDER BY y)
3199        if isinstance(func, exp.ArrayAgg):
3200            if not isinstance(order := expression.expression, exp.Order):
3201                return self.sql(func)
3202
3203            # Save the original column for FILTER clause (before wrapping with Order)
3204            original_this = func.this
3205
3206            # Move ORDER BY inside ARRAY_AGG by wrapping its argument with Order
3207            # ArrayAgg.this should become Order(this=ArrayAgg.this, expressions=order.expressions)
3208            func.set(
3209                "this",
3210                exp.Order(
3211                    this=func.this.copy(),
3212                    expressions=order.expressions,
3213                ),
3214            )
3215
3216            # Generate the ARRAY_AGG function with ORDER BY and add FILTER clause if needed
3217            # Use original_this (not the Order-wrapped version) for the FILTER condition
3218            array_agg_sql = self.function_fallback_sql(func)
3219            return self._add_arrayagg_null_filter(array_agg_sql, func, original_this)
3220
3221        # For other functions (like PERCENTILES), use existing logic
3222        expression_sql = self.sql(expression, "expression")
3223
3224        if isinstance(func, exp.PERCENTILES):
3225            # Make the order key the first arg and slide the fraction to the right
3226            # https://duckdb.org/docs/sql/aggregates#ordered-set-aggregate-functions
3227            order_col = expression.find(exp.Ordered)
3228            if order_col:
3229                func.set("expression", func.this)
3230                func.set("this", order_col.this)
3231
3232        this = self.sql(expression, "this").rstrip(")")
3233
3234        return f"{this}{expression_sql})"
3235
3236    def length_sql(self, expression: exp.Length) -> str:
3237        arg = expression.this
3238
3239        # Dialects like BQ and Snowflake also accept binary values as args, so
3240        # DDB will attempt to infer the type or resort to case/when resolution
3241        if not expression.args.get("binary") or arg.is_string:
3242            return self.func("LENGTH", arg)
3243
3244        if not arg.type:
3245            from sqlglot.optimizer.annotate_types import annotate_types
3246
3247            arg = annotate_types(arg, dialect=self.dialect)
3248
3249        if arg.is_type(*exp.DataType.TEXT_TYPES):
3250            return self.func("LENGTH", arg)
3251
3252        # We need these casts to make duckdb's static type checker happy
3253        blob = exp.cast(arg, exp.DType.VARBINARY)
3254        varchar = exp.cast(arg, exp.DType.VARCHAR)
3255
3256        case = (
3257            exp.case(exp.Anonymous(this="TYPEOF", expressions=[arg]))
3258            .when(exp.Literal.string("BLOB"), exp.ByteLength(this=blob))
3259            .else_(exp.Anonymous(this="LENGTH", expressions=[varchar]))
3260        )
3261        return self.sql(case)
3262
3263    def bitlength_sql(self, expression: exp.BitLength) -> str:
3264        if not _is_binary(arg := expression.this):
3265            return self.func("BIT_LENGTH", arg)
3266
3267        blob = exp.cast(arg, exp.DataType.Type.VARBINARY)
3268        return self.sql(exp.ByteLength(this=blob) * exp.Literal.number(8))
3269
3270    def chr_sql(self, expression: exp.Chr, name: str = "CHR") -> str:
3271        arg = expression.expressions[0]
3272        if arg.is_type(*exp.DataType.REAL_TYPES):
3273            arg = exp.cast(arg, exp.DType.INT)
3274        return self.func("CHR", arg)
3275
3276    def collation_sql(self, expression: exp.Collation) -> str:
3277        self.unsupported("COLLATION function is not supported by DuckDB")
3278        return self.function_fallback_sql(expression)
3279
3280    def collate_sql(self, expression: exp.Collate) -> str:
3281        if not expression.expression.is_string:
3282            return super().collate_sql(expression)
3283
3284        raw = expression.expression.name
3285        if not raw:
3286            return self.sql(expression.this)
3287
3288        parts = []
3289        for part in raw.split("-"):
3290            lower = part.lower()
3291            if lower not in _SNOWFLAKE_COLLATION_DEFAULTS:
3292                if lower in _SNOWFLAKE_COLLATION_UNSUPPORTED:
3293                    self.unsupported(
3294                        f"Snowflake collation specifier '{part}' has no DuckDB equivalent"
3295                    )
3296                parts.append(lower)
3297
3298        if not parts:
3299            return self.sql(expression.this)
3300        return super().collate_sql(
3301            exp.Collate(this=expression.this, expression=exp.var(".".join(parts)))
3302        )
3303
3304    def _validate_regexp_flags(self, flags: exp.Expr | None, supported_flags: str) -> str | None:
3305        """
3306        Validate and filter regexp flags for DuckDB compatibility.
3307
3308        Args:
3309            flags: The flags expression to validate
3310            supported_flags: String of supported flags (e.g., "ims", "cims").
3311                            Only these flags will be returned.
3312
3313        Returns:
3314            Validated/filtered flag string, or None if no valid flags remain
3315        """
3316        if not isinstance(flags, exp.Expr):
3317            return None
3318
3319        if not flags.is_string:
3320            self.unsupported("Non-literal regexp flags are not fully supported in DuckDB")
3321            return None
3322
3323        flag_str = flags.this
3324        unsupported = set(flag_str) - set(supported_flags)
3325
3326        if unsupported:
3327            self.unsupported(
3328                f"Regexp flags {sorted(unsupported)} are not supported in this context"
3329            )
3330
3331        flag_str = "".join(f for f in flag_str if f in supported_flags)
3332        return flag_str if flag_str else None
3333
3334    def regexpcount_sql(self, expression: exp.RegexpCount) -> str:
3335        this = expression.this
3336        pattern = expression.expression
3337        position = expression.args.get("position")
3338        parameters = expression.args.get("parameters")
3339
3340        # Validate flags - only "ims" flags are supported for embedded patterns
3341        validated_flags = self._validate_regexp_flags(parameters, supported_flags="ims")
3342
3343        if position:
3344            this = exp.Substring(this=this, start=position)
3345
3346        # Embed flags in pattern (REGEXP_EXTRACT_ALL doesn't support flags argument)
3347        if validated_flags:
3348            pattern = exp.Concat(expressions=[exp.Literal.string(f"(?{validated_flags})"), pattern])
3349
3350        # Handle empty pattern: Snowflake returns 0, DuckDB would match between every character
3351        result = (
3352            exp.case()
3353            .when(
3354                exp.EQ(this=pattern, expression=exp.Literal.string("")),
3355                exp.Literal.number(0),
3356            )
3357            .else_(
3358                exp.Length(
3359                    this=exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern])
3360                )
3361            )
3362        )
3363
3364        return self.sql(result)
3365
3366    def regexpreplace_sql(self, expression: exp.RegexpReplace) -> str:
3367        subject = expression.this
3368        pattern = expression.expression
3369        replacement = expression.args.get("replacement") or exp.Literal.string("")
3370        position = expression.args.get("position")
3371        occurrence = expression.args.get("occurrence")
3372        modifiers = expression.args.get("modifiers")
3373
3374        validated_flags = self._validate_regexp_flags(modifiers, supported_flags="cimsg") or ""
3375
3376        # Handle occurrence (only literals supported)
3377        if occurrence and not occurrence.is_int:
3378            self.unsupported("REGEXP_REPLACE with non-literal occurrence")
3379        else:
3380            occurrence = occurrence.to_py() if occurrence and occurrence.is_int else 0
3381            if occurrence > 1:
3382                self.unsupported(f"REGEXP_REPLACE occurrence={occurrence} not supported")
3383            # flag duckdb to do either all or none, single_replace check is for duckdb round trip
3384            elif (
3385                occurrence == 0
3386                and "g" not in validated_flags
3387                and not expression.args.get("single_replace")
3388            ):
3389                validated_flags += "g"
3390
3391        # Handle position (only literals supported)
3392        prefix = None
3393        if position and not position.is_int:
3394            self.unsupported("REGEXP_REPLACE with non-literal position")
3395        elif position and position.is_int and position.to_py() > 1:
3396            pos = position.to_py()
3397            prefix = exp.Substring(
3398                this=subject, start=exp.Literal.number(1), length=exp.Literal.number(pos - 1)
3399            )
3400            subject = exp.Substring(this=subject, start=exp.Literal.number(pos))
3401
3402        result: exp.Expr = exp.Anonymous(
3403            this="REGEXP_REPLACE",
3404            expressions=[
3405                subject,
3406                pattern,
3407                replacement,
3408                exp.Literal.string(validated_flags) if validated_flags else None,
3409            ],
3410        )
3411
3412        if prefix:
3413            result = exp.Concat(expressions=[prefix, result])
3414
3415        return self.sql(result)
3416
3417    def regexplike_sql(self, expression: exp.RegexpLike) -> str:
3418        this = expression.this
3419        pattern = expression.expression
3420        flag = expression.args.get("flag")
3421
3422        if expression.args.get("full_match"):
3423            validated_flags = self._validate_regexp_flags(flag, supported_flags="cims")
3424            flag = exp.Literal.string(validated_flags) if validated_flags else None
3425            return self.func("REGEXP_FULL_MATCH", this, pattern, flag)
3426
3427        return self.func("REGEXP_MATCHES", this, pattern, flag)
3428
3429    @unsupported_args("ins_cost", "del_cost", "sub_cost")
3430    def levenshtein_sql(self, expression: exp.Levenshtein) -> str:
3431        this = expression.this
3432        expr = expression.expression
3433        max_dist = expression.args.get("max_dist")
3434
3435        if max_dist is None:
3436            return self.func("LEVENSHTEIN", this, expr)
3437
3438        # Emulate Snowflake semantics: if distance > max_dist, return max_dist
3439        levenshtein = exp.Levenshtein(this=this, expression=expr)
3440        return self.sql(exp.Least(this=levenshtein, expressions=[max_dist]))
3441
3442    def pad_sql(self, expression: exp.Pad) -> str:
3443        """
3444        Handle RPAD/LPAD for VARCHAR and BINARY types.
3445
3446        For VARCHAR: Delegate to parent class
3447        For BINARY: Lower to: input || REPEAT(pad, GREATEST(0, target_len - OCTET_LENGTH(input)))
3448        """
3449        string_arg = expression.this
3450        fill_arg = expression.args.get("fill_pattern") or exp.Literal.string(" ")
3451
3452        if _is_binary(string_arg) or _is_binary(fill_arg):
3453            length_arg = expression.expression
3454            is_left = expression.args.get("is_left")
3455
3456            input_len = exp.ByteLength(this=string_arg)
3457            chars_needed = length_arg - input_len
3458            pad_count = exp.Greatest(
3459                this=exp.Literal.number(0), expressions=[chars_needed], ignore_nulls=True
3460            )
3461            repeat_expr = exp.Repeat(this=fill_arg, times=pad_count)
3462
3463            left, right = string_arg, repeat_expr
3464            if is_left:
3465                left, right = right, left
3466
3467            result = exp.DPipe(this=left, expression=right)
3468            return self.sql(result)
3469
3470        # For VARCHAR: Delegate to parent class (handles PAD_FILL_PATTERN_IS_REQUIRED)
3471        return super().pad_sql(expression)
3472
3473    def minhash_sql(self, expression: exp.Minhash) -> str:
3474        k = expression.this
3475        exprs = expression.expressions
3476
3477        if len(exprs) != 1 or isinstance(exprs[0], exp.Star):
3478            self.unsupported(
3479                "MINHASH with multiple expressions or * requires manual query restructuring"
3480            )
3481            return self.func("MINHASH", k, *exprs)
3482
3483        expr = exprs[0]
3484        result = exp.replace_placeholders(self.MINHASH_TEMPLATE.copy(), expr=expr, k=k)
3485        return f"({self.sql(result)})"
3486
3487    def minhashcombine_sql(self, expression: exp.MinhashCombine) -> str:
3488        expr = expression.this
3489        result = exp.replace_placeholders(self.MINHASH_COMBINE_TEMPLATE.copy(), expr=expr)
3490        return f"({self.sql(result)})"
3491
3492    def approximatesimilarity_sql(self, expression: exp.ApproximateSimilarity) -> str:
3493        expr = expression.this
3494        result = exp.replace_placeholders(self.APPROXIMATE_SIMILARITY_TEMPLATE.copy(), expr=expr)
3495        return f"({self.sql(result)})"
3496
3497    def arrayuniqueagg_sql(self, expression: exp.ArrayUniqueAgg) -> str:
3498        return self.sql(
3499            exp.Filter(
3500                this=exp.func("LIST", exp.Distinct(expressions=[expression.this])),
3501                expression=exp.Where(this=expression.this.copy().is_(exp.null()).not_()),
3502            )
3503        )
3504
3505    def arrayconcatagg_sql(self, expression: exp.ArrayConcatAgg) -> str:
3506        this = expression.this
3507
3508        if isinstance(this, exp.Limit):
3509            self.unsupported("LIMIT in ARRAY_CONCAT_AGG cannot be transpiled to DuckDB")
3510            this = this.this
3511
3512        inner = this.this if isinstance(this, exp.Order) else this
3513
3514        return self.func(
3515            "FLATTEN",
3516            exp.Filter(
3517                this=exp.ArrayAgg(this=this),
3518                expression=exp.Where(this=inner.copy().is_(exp.null()).not_()),
3519            ),
3520        )
3521
3522    def arrayunionagg_sql(self, expression: exp.ArrayUnionAgg) -> str:
3523        self.unsupported("ARRAY_UNION_AGG is not supported in DuckDB")
3524        return self.function_fallback_sql(expression)
3525
3526    def arraydistinct_sql(self, expression: exp.ArrayDistinct) -> str:
3527        arr = expression.this
3528        func = self.func("LIST_DISTINCT", arr)
3529
3530        if expression.args.get("check_null"):
3531            add_null_to_array = exp.func(
3532                "LIST_APPEND", exp.func("LIST_DISTINCT", exp.ArrayCompact(this=arr)), exp.Null()
3533            )
3534            return self.sql(
3535                exp.If(
3536                    this=exp.NEQ(
3537                        this=exp.ArraySize(this=arr), expression=exp.func("LIST_COUNT", arr)
3538                    ),
3539                    true=add_null_to_array,
3540                    false=func,
3541                )
3542            )
3543
3544        return func
3545
3546    def arrayintersect_sql(self, expression: exp.ArrayIntersect) -> str:
3547        if expression.args.get("is_multiset") and len(expression.expressions) == 2:
3548            return self._array_bag_sql(
3549                self.ARRAY_INTERSECTION_CONDITION,
3550                expression.expressions[0],
3551                expression.expressions[1],
3552            )
3553        return self.function_fallback_sql(expression)
3554
3555    def arrayexcept_sql(self, expression: exp.ArrayExcept) -> str:
3556        arr1, arr2 = expression.this, expression.expression
3557        if expression.args.get("is_multiset"):
3558            return self._array_bag_sql(self.ARRAY_EXCEPT_CONDITION, arr1, arr2)
3559        return self.sql(
3560            exp.replace_placeholders(self.ARRAY_EXCEPT_SET_TEMPLATE, arr1=arr1, arr2=arr2)
3561        )
3562
3563    def arrayslice_sql(self, expression: exp.ArraySlice) -> str:
3564        """
3565        Transpiles Snowflake's ARRAY_SLICE (0-indexed, exclusive end) to DuckDB's
3566        ARRAY_SLICE (1-indexed, inclusive end) by wrapping start and end in CASE
3567        expressions that adjust the index at query time:
3568          - start: CASE WHEN start >= 0 THEN start + 1 ELSE start END
3569          - end:   CASE WHEN end < 0 THEN end - 1 ELSE end END
3570        """
3571        start, end = expression.args.get("start"), expression.args.get("end")
3572
3573        if expression.args.get("zero_based"):
3574            if start is not None:
3575                start = (
3576                    exp.case()
3577                    .when(
3578                        exp.GTE(this=start.copy(), expression=exp.Literal.number(0)),
3579                        exp.Add(this=start.copy(), expression=exp.Literal.number(1)),
3580                    )
3581                    .else_(start)
3582                )
3583            if end is not None:
3584                end = (
3585                    exp.case()
3586                    .when(
3587                        exp.LT(this=end.copy(), expression=exp.Literal.number(0)),
3588                        exp.Sub(this=end.copy(), expression=exp.Literal.number(1)),
3589                    )
3590                    .else_(end)
3591                )
3592
3593        return self.func("ARRAY_SLICE", expression.this, start, end, expression.args.get("step"))
3594
3595    def arrayszip_sql(self, expression: exp.ArraysZip) -> str:
3596        args = expression.expressions
3597
3598        if not args:
3599            # Return [{}] - using MAP([], []) since DuckDB can't represent empty structs
3600            return self.sql(exp.array(exp.Map(keys=exp.array(), values=exp.array())))
3601
3602        # Build placeholder values for template
3603        lengths = [exp.Length(this=arg) for arg in args]
3604        max_len = (
3605            lengths[0]
3606            if len(lengths) == 1
3607            else exp.Greatest(this=lengths[0], expressions=lengths[1:])
3608        )
3609
3610        # Empty struct with same schema: {'$1': NULL, '$2': NULL, ...}
3611        empty_struct = exp.func(
3612            "STRUCT",
3613            *[
3614                exp.PropertyEQ(this=exp.Literal.string(f"${i + 1}"), expression=exp.Null())
3615                for i in range(len(args))
3616            ],
3617        )
3618
3619        # Struct for transform: {'$1': COALESCE(arr1, [])[__i + 1], ...}
3620        # COALESCE wrapping handles NULL arrays - prevents invalid NULL[i] syntax
3621        index = exp.column("__i") + 1
3622        transform_struct = exp.func(
3623            "STRUCT",
3624            *[
3625                exp.PropertyEQ(
3626                    this=exp.Literal.string(f"${i + 1}"),
3627                    expression=exp.func("COALESCE", arg, exp.array())[index],
3628                )
3629                for i, arg in enumerate(args)
3630            ],
3631        )
3632
3633        result = exp.replace_placeholders(
3634            self.ARRAYS_ZIP_TEMPLATE.copy(),
3635            null_check=exp.or_(*[arg.is_(exp.Null()) for arg in args]),
3636            all_empty_check=exp.and_(
3637                *[
3638                    exp.EQ(this=exp.Length(this=arg), expression=exp.Literal.number(0))
3639                    for arg in args
3640                ]
3641            ),
3642            empty_struct=empty_struct,
3643            max_len=max_len,
3644            transform_struct=transform_struct,
3645        )
3646        return self.sql(result)
3647
3648    def lower_sql(self, expression: exp.Lower) -> str:
3649        result_sql = self.func("LOWER", _cast_to_varchar(expression.this))
3650        return _gen_with_cast_to_blob(self, expression, result_sql)
3651
3652    def upper_sql(self, expression: exp.Upper) -> str:
3653        result_sql = self.func("UPPER", _cast_to_varchar(expression.this))
3654        return _gen_with_cast_to_blob(self, expression, result_sql)
3655
3656    def reverse_sql(self, expression: exp.Reverse) -> str:
3657        result_sql = self.func("REVERSE", _cast_to_varchar(expression.this))
3658        return _gen_with_cast_to_blob(self, expression, result_sql)
3659
3660    def _left_right_sql(self, expression: exp.Left | exp.Right, func_name: str) -> str:
3661        arg = expression.this
3662        length = expression.expression
3663        is_binary = _is_binary(arg)
3664
3665        if is_binary:
3666            # LEFT/RIGHT(blob, n) becomes UNHEX(LEFT/RIGHT(HEX(blob), n * 2))
3667            # Each byte becomes 2 hex chars, so multiply length by 2
3668            hex_arg = exp.Hex(this=arg)
3669            hex_length = exp.Mul(this=length, expression=exp.Literal.number(2))
3670            result: exp.Expression = exp.Unhex(
3671                this=exp.Anonymous(this=func_name, expressions=[hex_arg, hex_length])
3672            )
3673        else:
3674            result = exp.Anonymous(this=func_name, expressions=[arg, length])
3675
3676        if expression.args.get("negative_length_returns_empty"):
3677            empty: exp.Expression = exp.Literal.string("")
3678            if is_binary:
3679                empty = exp.Unhex(this=empty)
3680            result = exp.case().when(length < exp.Literal.number(0), empty).else_(result)
3681
3682        return self.sql(result)
3683
3684    def left_sql(self, expression: exp.Left) -> str:
3685        return self._left_right_sql(expression, "LEFT")
3686
3687    def right_sql(self, expression: exp.Right) -> str:
3688        return self._left_right_sql(expression, "RIGHT")
3689
3690    def rtrimmedlength_sql(self, expression: exp.RtrimmedLength) -> str:
3691        return self.func("LENGTH", exp.Trim(this=expression.this, position="TRAILING"))
3692
3693    def stuff_sql(self, expression: exp.Stuff) -> str:
3694        base = expression.this
3695        start = expression.args["start"]
3696        length = expression.args["length"]
3697        insertion = expression.expression
3698        is_binary = _is_binary(base)
3699
3700        if is_binary:
3701            # DuckDB's SUBSTRING doesn't accept BLOB; operate on the HEX string instead
3702            # (each byte = 2 hex chars), then UNHEX back to BLOB
3703            base = exp.Hex(this=base)
3704            insertion = exp.Hex(this=insertion)
3705            left = exp.Substring(
3706                this=base.copy(),
3707                start=exp.Literal.number(1),
3708                length=(start.copy() - exp.Literal.number(1)) * exp.Literal.number(2),
3709            )
3710            right = exp.Substring(
3711                this=base.copy(),
3712                start=((start + length) - exp.Literal.number(1)) * exp.Literal.number(2)
3713                + exp.Literal.number(1),
3714            )
3715        else:
3716            left = exp.Substring(
3717                this=base.copy(),
3718                start=exp.Literal.number(1),
3719                length=start.copy() - exp.Literal.number(1),
3720            )
3721            right = exp.Substring(this=base.copy(), start=start + length)
3722        result: exp.Expr = exp.DPipe(
3723            this=exp.DPipe(this=left, expression=insertion), expression=right
3724        )
3725
3726        if is_binary:
3727            result = exp.Unhex(this=result)
3728
3729        return self.sql(result)
3730
3731    def rand_sql(self, expression: exp.Rand) -> str:
3732        seed = expression.this
3733        if seed is not None:
3734            self.unsupported("RANDOM with seed is not supported in DuckDB")
3735
3736        lower = expression.args.get("lower")
3737        upper = expression.args.get("upper")
3738
3739        if lower and upper:
3740            # scale DuckDB's [0,1) to the specified range
3741            range_size = exp.paren(upper - lower)
3742            scaled = exp.Add(this=lower, expression=exp.func("random") * range_size)
3743
3744            # For now we assume that if bounds are set, return type is BIGINT. Snowflake/Teradata
3745            result = exp.cast(scaled, exp.DType.BIGINT)
3746            return self.sql(result)
3747
3748        # Default DuckDB behavior - just return RANDOM() as float
3749        return "RANDOM()"
3750
3751    def bytelength_sql(self, expression: exp.ByteLength) -> str:
3752        arg = expression.this
3753
3754        # Check if it's a text type (handles both literals and annotated expressions)
3755        if arg.is_type(*exp.DataType.TEXT_TYPES):
3756            return self.func("OCTET_LENGTH", exp.Encode(this=arg))
3757
3758        # Default: pass through as-is (conservative for DuckDB, handles binary and unannotated)
3759        return self.func("OCTET_LENGTH", arg)
3760
3761    def base64encode_sql(self, expression: exp.Base64Encode) -> str:
3762        # DuckDB TO_BASE64 requires BLOB input
3763        # Snowflake BASE64_ENCODE accepts both VARCHAR and BINARY - for VARCHAR it implicitly
3764        # encodes UTF-8 bytes. We add ENCODE unless the input is a binary type.
3765        result = expression.this
3766
3767        # Check if input is a string type - ENCODE only accepts VARCHAR
3768        if result.is_type(*exp.DataType.TEXT_TYPES):
3769            result = exp.Encode(this=result)
3770
3771        result = exp.ToBase64(this=result)
3772
3773        max_line_length = expression.args.get("max_line_length")
3774        alphabet = expression.args.get("alphabet")
3775
3776        # Handle custom alphabet by replacing standard chars with custom ones
3777        result = _apply_base64_alphabet_replacements(result, alphabet)
3778
3779        # Handle max_line_length by inserting newlines every N characters
3780        line_length = (
3781            t.cast(int, max_line_length.to_py())
3782            if isinstance(max_line_length, exp.Literal) and max_line_length.is_number
3783            else 0
3784        )
3785        if line_length > 0:
3786            newline = exp.Chr(expressions=[exp.Literal.number(10)])
3787            result = exp.Trim(
3788                this=exp.RegexpReplace(
3789                    this=result,
3790                    expression=exp.Literal.string(f"(.{{{line_length}}})"),
3791                    replacement=exp.Concat(expressions=[exp.Literal.string("\\1"), newline.copy()]),
3792                ),
3793                expression=newline,
3794                position="TRAILING",
3795            )
3796
3797        return self.sql(result)
3798
3799    def hex_sql(self, expression: exp.Hex) -> str:
3800        case = expression.args.get("case")
3801
3802        if not case:
3803            return self.func("HEX", expression.this)
3804
3805        hex_expr = exp.Hex(this=expression.this)
3806        return self.sql(
3807            exp.case()
3808            .when(case.is_(exp.null()), exp.null())
3809            .when(case.copy().eq(0), exp.Lower(this=hex_expr.copy()))
3810            .else_(hex_expr)
3811        )
3812
3813    def replace_sql(self, expression: exp.Replace) -> str:
3814        result_sql = self.func(
3815            "REPLACE",
3816            _cast_to_varchar(expression.this),
3817            _cast_to_varchar(expression.expression),
3818            _cast_to_varchar(expression.args.get("replacement")),
3819        )
3820        return _gen_with_cast_to_blob(self, expression, result_sql)
3821
3822    def _bitwise_op(self, expression: exp.Binary, op: str) -> str:
3823        _prepare_binary_bitwise_args(expression)
3824        result_sql = self.binary(expression, op)
3825        return _gen_with_cast_to_blob(self, expression, result_sql)
3826
3827    def bitwisexor_sql(self, expression: exp.BitwiseXor) -> str:
3828        _prepare_binary_bitwise_args(expression)
3829        result_sql = self.func("XOR", expression.this, expression.expression)
3830        return _gen_with_cast_to_blob(self, expression, result_sql)
3831
3832    def objectinsert_sql(self, expression: exp.ObjectInsert) -> str:
3833        this = expression.this
3834        key = expression.args.get("key")
3835        key_sql = key.name if isinstance(key, exp.Expr) else ""
3836        value_sql = self.sql(expression, "value")
3837
3838        kv_sql = f"{key_sql} := {value_sql}"
3839
3840        # If the input struct is empty e.g. transpiling OBJECT_INSERT(OBJECT_CONSTRUCT(), key, value) from Snowflake
3841        # then we can generate STRUCT_PACK which will build it since STRUCT_INSERT({}, key := value) is not valid DuckDB
3842        if isinstance(this, exp.Struct) and not this.expressions:
3843            return self.func("STRUCT_PACK", kv_sql)
3844
3845        return self.func("STRUCT_INSERT", this, kv_sql)
3846
3847    def mapcat_sql(self, expression: exp.MapCat) -> str:
3848        result = exp.replace_placeholders(
3849            self.MAPCAT_TEMPLATE.copy(),
3850            map1=expression.this,
3851            map2=expression.expression,
3852        )
3853        return self.sql(result)
3854
3855    def mapcontainskey_sql(self, expression: exp.MapContainsKey) -> str:
3856        return self.func(
3857            "ARRAY_CONTAINS", exp.func("MAP_KEYS", expression.args["key"]), expression.this
3858        )
3859
3860    def mapdelete_sql(self, expression: exp.MapDelete) -> str:
3861        map_arg = expression.this
3862        keys_to_delete = expression.expressions
3863
3864        x_dot_key = exp.Dot(this=exp.to_identifier("x"), expression=exp.to_identifier("key"))
3865
3866        lambda_expr = exp.Lambda(
3867            this=exp.In(this=x_dot_key, expressions=keys_to_delete).not_(),
3868            expressions=[exp.to_identifier("x")],
3869        )
3870        result = exp.func(
3871            "MAP_FROM_ENTRIES",
3872            exp.ArrayFilter(this=exp.func("MAP_ENTRIES", map_arg), expression=lambda_expr),
3873        )
3874        return self.sql(result)
3875
3876    def mappick_sql(self, expression: exp.MapPick) -> str:
3877        map_arg = expression.this
3878        keys_to_pick = expression.expressions
3879
3880        x_dot_key = exp.Dot(this=exp.to_identifier("x"), expression=exp.to_identifier("key"))
3881
3882        if len(keys_to_pick) == 1 and keys_to_pick[0].is_type(exp.DType.ARRAY):
3883            lambda_expr = exp.Lambda(
3884                this=exp.func("ARRAY_CONTAINS", keys_to_pick[0], x_dot_key),
3885                expressions=[exp.to_identifier("x")],
3886            )
3887        else:
3888            lambda_expr = exp.Lambda(
3889                this=exp.In(this=x_dot_key, expressions=keys_to_pick),
3890                expressions=[exp.to_identifier("x")],
3891            )
3892
3893        result = exp.func(
3894            "MAP_FROM_ENTRIES",
3895            exp.func("LIST_FILTER", exp.func("MAP_ENTRIES", map_arg), lambda_expr),
3896        )
3897        return self.sql(result)
3898
3899    def mapsize_sql(self, expression: exp.MapSize) -> str:
3900        return self.func("CARDINALITY", expression.this)
3901
3902    @unsupported_args("update_flag")
3903    def mapinsert_sql(self, expression: exp.MapInsert) -> str:
3904        map_arg = expression.this
3905        key = expression.args.get("key")
3906        value = expression.args.get("value")
3907
3908        map_type = map_arg.type
3909
3910        if value is not None:
3911            if map_type and map_type.expressions and len(map_type.expressions) > 1:
3912                # Extract the value type from MAP(key_type, value_type)
3913                value_type = map_type.expressions[1]
3914                # Cast value to match the map's value type to avoid type conflicts
3915                value = exp.cast(value, value_type)
3916            # else: polymorphic MAP case - no type parameters available, use value as-is
3917
3918        # Create a single-entry map for the new key-value pair
3919        new_entry_struct = exp.Struct(expressions=[exp.PropertyEQ(this=key, expression=value)])
3920        new_entry: exp.Expression = exp.ToMap(this=new_entry_struct)
3921
3922        # Use MAP_CONCAT to merge the original map with the new entry
3923        # This automatically handles both insert and update cases
3924        result = exp.func("MAP_CONCAT", map_arg, new_entry)
3925
3926        return self.sql(result)
3927
3928    def startswith_sql(self, expression: exp.StartsWith) -> str:
3929        return self.func(
3930            "STARTS_WITH",
3931            _cast_to_varchar(expression.this),
3932            _cast_to_varchar(expression.expression),
3933        )
3934
3935    def space_sql(self, expression: exp.Space) -> str:
3936        # DuckDB's REPEAT requires BIGINT for the count parameter
3937        return self.sql(
3938            exp.Repeat(
3939                this=exp.Literal.string(" "),
3940                times=exp.cast(expression.this, exp.DType.BIGINT),
3941            )
3942        )
3943
3944    def tablefromrows_sql(self, expression: exp.TableFromRows) -> str:
3945        # For GENERATOR, unwrap TABLE() - just emit the Generator (becomes RANGE)
3946        if isinstance(expression.this, exp.Generator):
3947            # Preserve alias, joins, and other table-level args
3948            table = exp.Table(
3949                this=expression.this,
3950                alias=expression.args.get("alias"),
3951                joins=expression.args.get("joins"),
3952            )
3953            return self.sql(table)
3954
3955        return super().tablefromrows_sql(expression)
3956
3957    def unnest_sql(self, expression: exp.Unnest) -> str:
3958        explode_array = expression.args.get("explode_array")
3959        if explode_array:
3960            # In BigQuery, UNNESTing a nested array leads to explosion of the top-level array & struct
3961            # This is transpiled to DDB by transforming "FROM UNNEST(...)" to "FROM (SELECT UNNEST(..., max_depth => 2))"
3962            expression.expressions.append(
3963                exp.Kwarg(this=exp.var("max_depth"), expression=exp.Literal.number(2))
3964            )
3965
3966            # If BQ's UNNEST is aliased, we transform it from a column alias to a table alias in DDB
3967            alias = expression.args.get("alias")
3968            if isinstance(alias, exp.TableAlias):
3969                expression.set("alias", None)
3970                if alias.columns:
3971                    alias = exp.TableAlias(this=seq_get(alias.columns, 0))
3972
3973            unnest_sql = super().unnest_sql(expression)
3974            select = exp.Select(expressions=[unnest_sql]).subquery(alias)
3975            return self.sql(select)
3976
3977        return super().unnest_sql(expression)
3978
3979    def arrayagg_sql(self, expression: exp.ArrayAgg) -> str:
3980        if isinstance(expression.this, exp.Limit):
3981            self.unsupported("LIMIT inside ARRAY_AGG is not supported in DuckDB")
3982
3983        return super().arrayagg_sql(expression)
3984
3985    def ignorenulls_sql(self, expression: exp.IgnoreNulls) -> str:
3986        this = expression.this
3987
3988        if isinstance(this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
3989            # DuckDB should render IGNORE NULLS only for the general-purpose
3990            # window functions that accept it e.g. FIRST_VALUE(... IGNORE NULLS) OVER (...)
3991            return super().ignorenulls_sql(expression)
3992
3993        # For ARRAY_AGG(expr IGNORE NULLS ...), convert IGNORE NULLS to a
3994        # FILTER(WHERE expr IS NOT NULL) clause by setting nulls_excluded on
3995        # the ArrayAgg.  The existing _add_arrayagg_null_filter method will
3996        # emit the FILTER clause during arrayagg_sql / withingroup_sql.
3997        if isinstance(this, exp.ArrayAgg):
3998            this.set("nulls_excluded", True)
3999            return self.sql(this)
4000
4001        if isinstance(this, exp.First):
4002            this = exp.AnyValue(this=this.this)
4003
4004        if not isinstance(this, (exp.AnyValue, exp.ApproxQuantiles)):
4005            self.unsupported("IGNORE NULLS is not supported for non-window functions.")
4006
4007        return self.sql(this)
4008
4009    def split_sql(self, expression: exp.Split) -> str:
4010        base_func = exp.func("STR_SPLIT", expression.this, expression.expression)
4011
4012        case_expr = exp.case().else_(base_func)
4013        needs_case = False
4014
4015        if expression.args.get("null_returns_null"):
4016            case_expr = case_expr.when(expression.expression.is_(exp.null()), exp.null())
4017            needs_case = True
4018
4019        if expression.args.get("empty_delimiter_returns_whole"):
4020            # When delimiter is empty string, return input string as single array element
4021            array_with_input = exp.array(expression.this)
4022            case_expr = case_expr.when(
4023                expression.expression.eq(exp.Literal.string("")), array_with_input
4024            )
4025            needs_case = True
4026
4027        return self.sql(case_expr if needs_case else base_func)
4028
4029    def splitpart_sql(self, expression: exp.SplitPart) -> str:
4030        string_arg = expression.this
4031        delimiter_arg = expression.args.get("delimiter")
4032        part_index_arg = expression.args.get("part_index")
4033
4034        if delimiter_arg and part_index_arg:
4035            # Handle Snowflake's "index 0 and 1 both return first element" behavior
4036            if expression.args.get("part_index_zero_as_one"):
4037                # Convert 0 to 1 for compatibility
4038
4039                part_index_arg = exp.Paren(
4040                    this=exp.case()
4041                    .when(part_index_arg.eq(exp.Literal.number("0")), exp.Literal.number("1"))
4042                    .else_(part_index_arg)
4043                )
4044
4045            # Use Anonymous to avoid recursion
4046            base_func_expr: exp.Expr = exp.Anonymous(
4047                this="SPLIT_PART", expressions=[string_arg, delimiter_arg, part_index_arg]
4048            )
4049            needs_case_transform = False
4050            case_expr = exp.case().else_(base_func_expr)
4051
4052            if expression.args.get("empty_delimiter_returns_whole"):
4053                # When delimiter is empty string:
4054                # - Return whole string if part_index is 1 or -1
4055                # - Return empty string otherwise
4056                empty_case = exp.Paren(
4057                    this=exp.case()
4058                    .when(
4059                        exp.or_(
4060                            part_index_arg.eq(exp.Literal.number("1")),
4061                            part_index_arg.eq(exp.Literal.number("-1")),
4062                        ),
4063                        string_arg,
4064                    )
4065                    .else_(exp.Literal.string(""))
4066                )
4067
4068                case_expr = case_expr.when(delimiter_arg.eq(exp.Literal.string("")), empty_case)
4069                needs_case_transform = True
4070
4071            """
4072            Output looks something like this:
4073
4074            CASE
4075            WHEN delimiter is '' THEN
4076                (
4077                    CASE
4078                    WHEN adjusted_part_index = 1 OR adjusted_part_index = -1 THEN input
4079                    ELSE '' END
4080                )
4081            ELSE SPLIT_PART(input, delimiter, adjusted_part_index)
4082            END
4083
4084            """
4085            return self.sql(case_expr if needs_case_transform else base_func_expr)
4086
4087        return self.function_fallback_sql(expression)
4088
4089    def respectnulls_sql(self, expression: exp.RespectNulls) -> str:
4090        if isinstance(expression.this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
4091            # DuckDB should render RESPECT NULLS only for the general-purpose
4092            # window functions that accept it e.g. FIRST_VALUE(... RESPECT NULLS) OVER (...)
4093            return super().respectnulls_sql(expression)
4094
4095        self.unsupported("RESPECT NULLS is not supported for non-window functions.")
4096        return self.sql(expression, "this")
4097
4098    def arraytostring_sql(self, expression: exp.ArrayToString) -> str:
4099        null = expression.args.get("null")
4100
4101        if expression.args.get("null_is_empty"):
4102            x = exp.to_identifier("x")
4103            list_transform = exp.Transform(
4104                this=expression.this.copy(),
4105                expression=exp.Lambda(
4106                    this=exp.Coalesce(
4107                        this=exp.cast(x, "TEXT"), expressions=[exp.Literal.string("")]
4108                    ),
4109                    expressions=[x],
4110                ),
4111            )
4112            array_to_string = exp.ArrayToString(
4113                this=list_transform, expression=expression.expression
4114            )
4115            if expression.args.get("null_delim_is_null"):
4116                return self.sql(
4117                    exp.case()
4118                    .when(expression.expression.copy().is_(exp.null()), exp.null())
4119                    .else_(array_to_string)
4120                )
4121            return self.sql(array_to_string)
4122
4123        if null:
4124            x = exp.to_identifier("x")
4125            return self.sql(
4126                exp.ArrayToString(
4127                    this=exp.Transform(
4128                        this=expression.this,
4129                        expression=exp.Lambda(
4130                            this=exp.Coalesce(this=x, expressions=[null]),
4131                            expressions=[x],
4132                        ),
4133                    ),
4134                    expression=expression.expression,
4135                )
4136            )
4137
4138        return self.func("ARRAY_TO_STRING", expression.this, expression.expression)
4139
4140    def concatws_sql(self, expression: exp.ConcatWs) -> str:
4141        # DuckDB-specific: handle binary types using DPipe (||) operator
4142        separator = seq_get(expression.expressions, 0)
4143        args = expression.expressions[1:]
4144
4145        if any(_is_binary(arg) for arg in [separator, *args]):
4146            result = args[0]
4147            for arg in args[1:]:
4148                result = exp.DPipe(
4149                    this=exp.DPipe(this=result, expression=separator), expression=arg
4150                )
4151            return self.sql(result)
4152
4153        return super().concatws_sql(expression)
4154
4155    def _regexp_extract_sql(self, expression: exp.RegexpExtract | exp.RegexpExtractAll) -> str:
4156        this = expression.this
4157        group = expression.args.get("group")
4158        params = expression.args.get("parameters")
4159        position = expression.args.get("position")
4160        occurrence = expression.args.get("occurrence")
4161        null_if_pos_overflow = expression.args.get("null_if_pos_overflow")
4162
4163        # Handle Snowflake's 'e' flag: it enables capture group extraction
4164        # In DuckDB, this is controlled by the group parameter directly
4165        if params and params.is_string and "e" in params.name:
4166            params = exp.Literal.string(params.name.replace("e", ""))
4167
4168        validated_flags = self._validate_regexp_flags(params, supported_flags="cims")
4169
4170        # Strip default group when no following params (DuckDB default is same as group=0)
4171        if (
4172            not validated_flags
4173            and group
4174            and group.name == str(self.dialect.REGEXP_EXTRACT_DEFAULT_GROUP)
4175        ):
4176            group = None
4177
4178        flags_expr = exp.Literal.string(validated_flags) if validated_flags else None
4179
4180        # use substring to handle position argument
4181        if position and (not position.is_int or position.to_py() > 1):
4182            this = exp.Substring(this=this, start=position)
4183
4184            if null_if_pos_overflow:
4185                this = exp.Nullif(this=this, expression=exp.Literal.string(""))
4186
4187        is_extract_all = isinstance(expression, exp.RegexpExtractAll)
4188        non_single_occurrence = occurrence and (not occurrence.is_int or occurrence.to_py() > 1)
4189
4190        if is_extract_all or non_single_occurrence:
4191            name = "REGEXP_EXTRACT_ALL"
4192        else:
4193            name = "REGEXP_EXTRACT"
4194
4195        result: exp.Expr = exp.Anonymous(
4196            this=name, expressions=[this, expression.expression, group, flags_expr]
4197        )
4198
4199        # Array slicing for REGEXP_EXTRACT_ALL with occurrence
4200        if is_extract_all and non_single_occurrence:
4201            result = exp.Bracket(this=result, expressions=[exp.Slice(this=occurrence)])
4202        # ARRAY_EXTRACT for REGEXP_EXTRACT with occurrence > 1
4203        elif non_single_occurrence:
4204            result = exp.Anonymous(this="ARRAY_EXTRACT", expressions=[result, occurrence])
4205
4206        return self.sql(result)
4207
4208    def regexpextract_sql(self, expression: exp.RegexpExtract) -> str:
4209        return self._regexp_extract_sql(expression)
4210
4211    def regexpextractall_sql(self, expression: exp.RegexpExtractAll) -> str:
4212        return self._regexp_extract_sql(expression)
4213
4214    def regexpinstr_sql(self, expression: exp.RegexpInstr) -> str:
4215        this = expression.this
4216        pattern = expression.expression
4217        position = expression.args.get("position")
4218        orig_occ = expression.args.get("occurrence")
4219        occurrence = orig_occ or exp.Literal.number(1)
4220        option = expression.args.get("option")
4221        parameters = expression.args.get("parameters")
4222
4223        validated_flags = self._validate_regexp_flags(parameters, supported_flags="ims")
4224        if validated_flags:
4225            pattern = exp.Concat(expressions=[exp.Literal.string(f"(?{validated_flags})"), pattern])
4226
4227        # Handle starting position offset
4228        pos_offset: exp.Expr = exp.Literal.number(0)
4229        if position and (not position.is_int or position.to_py() > 1):
4230            this = exp.Substring(this=this, start=position)
4231            pos_offset = position - exp.Literal.number(1)
4232
4233        # Helper: LIST_SUM(LIST_TRANSFORM(list[1:end], x -> LENGTH(x)))
4234        def sum_lengths(func_name: str, end: exp.Expr) -> exp.Expr:
4235            lst = exp.Bracket(
4236                this=exp.Anonymous(this=func_name, expressions=[this, pattern]),
4237                expressions=[exp.Slice(this=exp.Literal.number(1), expression=end)],
4238                offset=1,
4239            )
4240            transform = exp.Anonymous(
4241                this="LIST_TRANSFORM",
4242                expressions=[
4243                    lst,
4244                    exp.Lambda(
4245                        this=exp.Length(this=exp.to_identifier("x")),
4246                        expressions=[exp.to_identifier("x")],
4247                    ),
4248                ],
4249            )
4250            return exp.Coalesce(
4251                this=exp.Anonymous(this="LIST_SUM", expressions=[transform]),
4252                expressions=[exp.Literal.number(0)],
4253            )
4254
4255        # Position = 1 + sum(split_lengths[1:occ]) + sum(match_lengths[1:occ-1]) + offset
4256        base_pos: exp.Expr = (
4257            exp.Literal.number(1)
4258            + sum_lengths("STRING_SPLIT_REGEX", occurrence)
4259            + sum_lengths("REGEXP_EXTRACT_ALL", occurrence - exp.Literal.number(1))
4260            + pos_offset
4261        )
4262
4263        # option=1: add match length for end position
4264        if option and option.is_int and option.to_py() == 1:
4265            match_at_occ = exp.Bracket(
4266                this=exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern]),
4267                expressions=[occurrence],
4268                offset=1,
4269            )
4270            base_pos = base_pos + exp.Coalesce(
4271                this=exp.Length(this=match_at_occ), expressions=[exp.Literal.number(0)]
4272            )
4273
4274        # NULL checks for all provided arguments
4275        # .copy() is used strictly because .is_() alters the node's parent pointer, mutating the parsed AST
4276        null_args = [
4277            expression.this,
4278            expression.expression,
4279            position,
4280            orig_occ,
4281            option,
4282            parameters,
4283        ]
4284        null_checks = [arg.copy().is_(exp.Null()) for arg in null_args if arg]
4285
4286        matches = exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern])
4287
4288        return self.sql(
4289            exp.case()
4290            .when(exp.or_(*null_checks), exp.Null())
4291            .when(pattern.copy().eq(exp.Literal.string("")), exp.Literal.number(0))
4292            .when(exp.Length(this=matches) < occurrence, exp.Literal.number(0))
4293            .else_(base_pos)
4294        )
4295
4296    @unsupported_args("culture")
4297    def numbertostr_sql(self, expression: exp.NumberToStr) -> str:
4298        fmt = expression.args.get("format")
4299        if fmt and fmt.is_int:
4300            return self.func("FORMAT", f"'{{:,.{fmt.name}f}}'", expression.this)
4301
4302        self.unsupported("Only integer formats are supported by NumberToStr")
4303        return self.function_fallback_sql(expression)
4304
4305    def autoincrementcolumnconstraint_sql(self, _) -> str:
4306        self.unsupported("The AUTOINCREMENT column constraint is not supported by DuckDB")
4307        return ""
4308
4309    def aliases_sql(self, expression: exp.Aliases) -> str:
4310        this = expression.this
4311        if isinstance(this, exp.Posexplode):
4312            return self.posexplode_sql(this)
4313
4314        return super().aliases_sql(expression)
4315
4316    def posexplode_sql(self, expression: exp.Posexplode) -> str:
4317        this = expression.this
4318        parent = expression.parent
4319
4320        # The default Spark aliases are "pos" and "col", unless specified otherwise
4321        pos, col = exp.to_identifier("pos"), exp.to_identifier("col")
4322
4323        if isinstance(parent, exp.Aliases):
4324            # Column case: SELECT POSEXPLODE(col) [AS (a, b)]
4325            pos, col = parent.expressions
4326        elif isinstance(parent, exp.Table):
4327            # Table case: SELECT * FROM POSEXPLODE(col) [AS (a, b)]
4328            alias = parent.args.get("alias")
4329            if alias:
4330                pos, col = alias.columns or [pos, col]
4331                alias.pop()
4332
4333        # Translate POSEXPLODE to UNNEST + GENERATE_SUBSCRIPTS
4334        # Note: In Spark pos is 0-indexed, but in DuckDB it's 1-indexed, so we subtract 1 from GENERATE_SUBSCRIPTS
4335        unnest_sql = self.sql(exp.Unnest(expressions=[this], alias=col))
4336        gen_subscripts = self.sql(
4337            exp.Alias(
4338                this=exp.Anonymous(
4339                    this="GENERATE_SUBSCRIPTS", expressions=[this, exp.Literal.number(1)]
4340                )
4341                - exp.Literal.number(1),
4342                alias=pos,
4343            )
4344        )
4345
4346        posexplode_sql = self.format_args(gen_subscripts, unnest_sql)
4347
4348        if isinstance(parent, exp.From) or (parent and isinstance(parent.parent, exp.From)):
4349            # SELECT * FROM POSEXPLODE(col) -> SELECT * FROM (SELECT GENERATE_SUBSCRIPTS(...), UNNEST(...))
4350            return self.sql(exp.Subquery(this=exp.Select(expressions=[posexplode_sql])))
4351
4352        return posexplode_sql
4353
4354    def addmonths_sql(self, expression: exp.AddMonths) -> str:
4355        """
4356        Handles three key issues:
4357        1. Float/decimal months: e.g., Snowflake rounds, whereas DuckDB INTERVAL requires integers
4358        2. End-of-month preservation: If input is last day of month, result is last day of result month
4359        3. Type preservation: Maintains DATE/TIMESTAMPTZ types (DuckDB defaults to TIMESTAMP)
4360        """
4361        from sqlglot.optimizer.annotate_types import annotate_types
4362
4363        this = expression.this
4364        if not this.type:
4365            this = annotate_types(this, dialect=self.dialect)
4366
4367        if this.is_type(*exp.DataType.TEXT_TYPES):
4368            this = exp.Cast(this=this, to=exp.DataType(this=exp.DType.TIMESTAMP))
4369
4370        # Detect float/decimal months to apply rounding (Snowflake behavior)
4371        # DuckDB INTERVAL syntax doesn't support non-integer expressions, so use TO_MONTHS
4372        months_expr = expression.expression
4373        if not months_expr.type:
4374            months_expr = annotate_types(months_expr, dialect=self.dialect)
4375
4376        # Build interval or to_months expression based on type
4377        # Float/decimal case: Round and use TO_MONTHS(CAST(ROUND(value) AS INT))
4378        interval_or_to_months = (
4379            exp.func("TO_MONTHS", exp.cast(exp.func("ROUND", months_expr), "INT"))
4380            if months_expr.is_type(
4381                exp.DType.FLOAT,
4382                exp.DType.DOUBLE,
4383                exp.DType.DECIMAL,
4384            )
4385            # Integer case: standard INTERVAL N MONTH syntax
4386            else exp.Interval(this=months_expr, unit=exp.var("MONTH"))
4387        )
4388
4389        date_add_expr = exp.Add(this=this, expression=interval_or_to_months)
4390
4391        # Apply end-of-month preservation if Snowflake flag is set
4392        # CASE WHEN LAST_DAY(date) = date THEN LAST_DAY(result) ELSE result END
4393        preserve_eom = expression.args.get("preserve_end_of_month")
4394        result_expr = (
4395            exp.case()
4396            .when(
4397                exp.EQ(this=exp.func("LAST_DAY", this), expression=this),
4398                exp.func("LAST_DAY", date_add_expr),
4399            )
4400            .else_(date_add_expr)
4401            if preserve_eom
4402            else date_add_expr
4403        )
4404
4405        # DuckDB's DATE_ADD function returns TIMESTAMP/DATETIME by default, even when the input is DATE
4406        # To match for example Snowflake's ADD_MONTHS behavior (which preserves the input type)
4407        # We need to cast the result back to the original type when the input is DATE or TIMESTAMPTZ
4408        # Example: ADD_MONTHS('2023-01-31'::date, 1) should return DATE, not TIMESTAMP
4409        if this.is_type(exp.DType.DATE, exp.DType.TIMESTAMPTZ):
4410            return self.sql(exp.Cast(this=result_expr, to=this.type))
4411        return self.sql(result_expr)
4412
4413    def format_sql(self, expression: exp.Format) -> str:
4414        if expression.name.lower() == "%s" and len(expression.expressions) == 1:
4415            return self.func("FORMAT", "'{}'", expression.expressions[0])
4416
4417        return self.function_fallback_sql(expression)
4418
4419    def hexstring_sql(
4420        self, expression: exp.HexString, binary_function_repr: str | None = None
4421    ) -> str:
4422        # UNHEX('FF') correctly produces blob \xFF in DuckDB
4423        return super().hexstring_sql(expression, binary_function_repr="UNHEX")
4424
4425    def datetrunc_sql(self, expression: exp.DateTrunc) -> str:
4426        unit = expression.args.get("unit")
4427        date = expression.this
4428
4429        week_start = _week_trunc_start_dow(unit)
4430        unit = unit_to_str(expression)
4431
4432        if week_start:
4433            result = self.sql(
4434                _build_week_trunc_expression(date, week_start, preserve_start_day=True)
4435            )
4436        else:
4437            result = self.func("DATE_TRUNC", unit, date)
4438
4439        if (
4440            expression.args.get("input_type_preserved")
4441            and date.is_type(*exp.DataType.TEMPORAL_TYPES)
4442            and not (is_date_unit(unit) and date.is_type(exp.DType.DATE))
4443        ):
4444            return self.sql(exp.Cast(this=result, to=date.type))
4445
4446        return result
4447
4448    def datetimetrunc_sql(self, expression: exp.DatetimeTrunc) -> str:
4449        this = exp.cast(expression.this, exp.DType.DATETIME)
4450        week_start = _week_trunc_start_dow(expression.args.get("unit"))
4451        if week_start:
4452            return self.sql(
4453                _build_week_trunc_expression(
4454                    this, week_start, preserve_start_day=True, cast_to_date=False
4455                )
4456            )
4457
4458        return self.func("DATE_TRUNC", unit_to_str(expression), this)
4459
4460    def timestamptrunc_sql(self, expression: exp.TimestampTrunc) -> str:
4461        zone = expression.args.get("zone")
4462        timestamp = expression.this
4463        week_start = _week_trunc_start_dow(expression.args.get("unit"))
4464
4465        # The week start emulation below is exact, so avoid weekstart_unit_to_str's degrade warning
4466        unit = unit_to_str(expression) if week_start else weekstart_unit_to_str(self, expression)
4467        date_unit = is_date_unit(unit) or bool(week_start)
4468
4469        def _trunc_expr(this: exp.Expr) -> exp.Expr:
4470            if week_start:
4471                return _build_week_trunc_expression(
4472                    this, week_start, preserve_start_day=True, cast_to_date=False
4473                )
4474            return exp.func("DATE_TRUNC", unit, this)
4475
4476        if date_unit and zone:
4477            # BigQuery's TIMESTAMP_TRUNC with timezone truncates in the target timezone and returns as UTC.
4478            # Double AT TIME ZONE needed for BigQuery compatibility:
4479            # 1. First AT TIME ZONE: ensures truncation happens in the target timezone
4480            # 2. Second AT TIME ZONE: converts the DATE result back to TIMESTAMPTZ (preserving time component)
4481            timestamp = exp.AtTimeZone(this=timestamp, zone=zone)
4482            trunced = _trunc_expr(timestamp)
4483            if isinstance(trunced, exp.DateAdd):
4484                # Parenthesize so the trailing AT TIME ZONE binds to the whole shifted expression
4485                trunced = exp.Paren(this=trunced)
4486            return self.sql(exp.AtTimeZone(this=trunced, zone=zone))
4487
4488        result = self.sql(_trunc_expr(timestamp))
4489        if expression.args.get("input_type_preserved"):
4490            if timestamp.type and timestamp.is_type(exp.DType.TIME, exp.DType.TIMETZ):
4491                dummy_date = exp.Cast(
4492                    this=exp.Literal.string("1970-01-01"),
4493                    to=exp.DataType(this=exp.DType.DATE),
4494                )
4495                date_time = exp.Add(this=dummy_date, expression=timestamp)
4496                result = self.func("DATE_TRUNC", unit, date_time)
4497                return self.sql(exp.Cast(this=result, to=timestamp.type))
4498
4499            if timestamp.is_type(*exp.DataType.TEMPORAL_TYPES) and not (
4500                date_unit and timestamp.is_type(exp.DType.DATE)
4501            ):
4502                return self.sql(exp.Cast(this=result, to=timestamp.type))
4503
4504        return result
4505
4506    def trim_sql(self, expression: exp.Trim) -> str:
4507        expression.this.replace(_cast_to_varchar(expression.this))
4508        if expression.expression:
4509            expression.expression.replace(_cast_to_varchar(expression.expression))
4510
4511        result_sql = super().trim_sql(expression)
4512        return _gen_with_cast_to_blob(self, expression, result_sql)
4513
4514    def round_sql(self, expression: exp.Round) -> str:
4515        this = expression.this
4516        decimals = expression.args.get("decimals")
4517        truncate = expression.args.get("truncate")
4518
4519        # DuckDB requires the scale (decimals) argument to be an INT
4520        # Some dialects (e.g., Snowflake) allow non-integer scales and cast to an integer internally
4521        if decimals is not None and expression.args.get("casts_non_integer_decimals"):
4522            if not (decimals.is_int or decimals.is_type(*exp.DataType.INTEGER_TYPES)):
4523                decimals = exp.cast(decimals, exp.DType.INT)
4524
4525        func = "ROUND"
4526        if truncate:
4527            # BigQuery uses ROUND_HALF_EVEN; Snowflake uses HALF_TO_EVEN
4528            if truncate.this in ("ROUND_HALF_EVEN", "HALF_TO_EVEN"):
4529                func = "ROUND_EVEN"
4530                truncate = None
4531            # BigQuery uses ROUND_HALF_AWAY_FROM_ZERO; Snowflake uses HALF_AWAY_FROM_ZERO
4532            elif truncate.this in ("ROUND_HALF_AWAY_FROM_ZERO", "HALF_AWAY_FROM_ZERO"):
4533                truncate = None
4534
4535        return self.func(func, this, decimals, truncate)
4536
4537    def trycast_sql(self, expression: exp.TryCast) -> str:
4538        to = expression.to
4539        to_type = to.this
4540        src = expression.this
4541
4542        if (
4543            expression.args.get("null_on_text_overflow")
4544            and to_type in exp.DataType.TEXT_TYPES
4545            and to.expressions
4546        ):
4547            return self.sql(
4548                exp.case()
4549                .when(
4550                    exp.LTE(this=exp.func("LENGTH", src), expression=to.expressions[0].this),
4551                    exp.cast(src, "TEXT"),
4552                )
4553                .else_(exp.Null())
4554            )
4555        elif to_type == exp.DType.DATE and expression.args.get("probe_date_format"):
4556            slash_strptime = exp.cast(
4557                exp.func("TRY_STRPTIME", src, exp.Literal.string(self._TRYCAST_DATE_SLASH_FMT)),
4558                "DATE",
4559            )
4560            mon_strptime = exp.cast(
4561                exp.func("TRY_STRPTIME", src, exp.Literal.string(self._TRYCAST_DATE_MON_FMT)),
4562                "DATE",
4563            )
4564            return self.sql(
4565                exp.case()
4566                .when(exp.func("CONTAINS", src, exp.Literal.string("/")), slash_strptime)
4567                .when(
4568                    exp.RegexpLike(this=src, expression=exp.Literal.string("[A-Za-z]")),
4569                    mon_strptime,
4570                )
4571                .else_(exp.TryCast(this=src, to=to))
4572            )
4573        elif (
4574            isinstance(to_type, exp.Interval)
4575            and (unit := to_type.unit)
4576            and expression.args.get("requires_string")
4577        ):
4578            interval_type = exp.DataType.build("INTERVAL")
4579            if isinstance(unit, exp.IntervalSpan):
4580                self.unsupported(
4581                    "TRY_CAST to INTERVAL with span (e.g. HOUR TO MINUTE) is not supported in DuckDB"
4582                )
4583                return self.sql(exp.TryCast(this=src, to=interval_type))
4584            return self.sql(
4585                exp.TryCast(
4586                    this=exp.DPipe(this=src, expression=exp.Literal.string(f" {unit.name}")),
4587                    to=interval_type,
4588                )
4589            )
4590
4591        return super().trycast_sql(expression)
4592
4593    def strtok_sql(self, expression: exp.Strtok) -> str:
4594        string_arg = expression.this
4595        delimiter_arg = expression.args.get("delimiter")
4596        part_index_arg = expression.args.get("part_index")
4597
4598        if delimiter_arg and part_index_arg:
4599            # Escape regex chars and build character class at runtime using REGEXP_REPLACE
4600            escaped_delimiter = exp.Anonymous(
4601                this="REGEXP_REPLACE",
4602                expressions=[
4603                    delimiter_arg,
4604                    exp.Literal.string(
4605                        r"([\[\]^.\-*+?(){}|$\\])"
4606                    ),  # Escape problematic regex chars
4607                    exp.Literal.string(
4608                        r"\\\1"
4609                    ),  # Replace with escaped version using $1 backreference
4610                    exp.Literal.string("g"),  # Global flag
4611                ],
4612            )
4613            # CASE WHEN delimiter = '' THEN '' ELSE CONCAT('[', escaped_delimiter, ']') END
4614            regex_pattern = (
4615                exp.case()
4616                .when(delimiter_arg.eq(exp.Literal.string("")), exp.Literal.string(""))
4617                .else_(
4618                    exp.func(
4619                        "CONCAT",
4620                        exp.Literal.string("["),
4621                        escaped_delimiter,
4622                        exp.Literal.string("]"),
4623                    )
4624                )
4625            )
4626
4627            # STRTOK skips empty strings, so we need to filter them out
4628            # LIST_FILTER(REGEXP_SPLIT_TO_ARRAY(string, pattern), x -> x != '')[index]
4629            split_array = exp.func("REGEXP_SPLIT_TO_ARRAY", string_arg, regex_pattern)
4630            x = exp.to_identifier("x")
4631            is_empty = x.eq(exp.Literal.string(""))
4632            filtered_array = exp.func(
4633                "LIST_FILTER",
4634                split_array,
4635                exp.Lambda(this=exp.not_(is_empty.copy()), expressions=[x.copy()]),
4636            )
4637            base_func = exp.Bracket(
4638                this=filtered_array,
4639                expressions=[part_index_arg],
4640                offset=1,
4641            )
4642
4643            # Use template with the built regex pattern
4644            result = exp.replace_placeholders(
4645                self.STRTOK_TEMPLATE.copy(),
4646                string=string_arg,
4647                delimiter=delimiter_arg,
4648                part_index=part_index_arg,
4649                base_func=base_func,
4650            )
4651
4652            return self.sql(result)
4653
4654        return self.function_fallback_sql(expression)
4655
4656    def strtoktoarray_sql(self, expression: exp.StrtokToArray) -> str:
4657        string_arg = expression.this
4658        delimiter_arg = expression.args.get("expression") or exp.Literal.string(" ")
4659
4660        escaped = exp.RegexpReplace(
4661            this=delimiter_arg.copy(),
4662            expression=exp.Literal.string(r"([\[\]^.\-*+?(){}|$\\])"),
4663            replacement=exp.Literal.string(r"\\\1"),
4664            modifiers=exp.Literal.string("g"),
4665        )
4666        return self.sql(
4667            exp.replace_placeholders(
4668                self.STRTOK_TO_ARRAY_TEMPLATE.copy(),
4669                string=string_arg,
4670                delimiter=delimiter_arg,
4671                escaped=escaped,
4672            )
4673        )
4674
4675    def approxquantile_sql(self, expression: exp.ApproxQuantile) -> str:
4676        result = self.func("APPROX_QUANTILE", expression.this, expression.args.get("quantile"))
4677
4678        # DuckDB returns integers for APPROX_QUANTILE, cast to DOUBLE if the expected type is a real type
4679        if expression.is_type(*exp.DataType.REAL_TYPES):
4680            result = f"CAST({result} AS DOUBLE)"
4681
4682        return result
4683
4684    def approxquantiles_sql(self, expression: exp.ApproxQuantiles) -> str:
4685        """
4686        BigQuery's APPROX_QUANTILES(expr, n) returns an array of n+1 approximate quantile values
4687        dividing the input distribution into n equal-sized buckets.
4688
4689        Both BigQuery and DuckDB use approximate algorithms for quantile estimation, but BigQuery
4690        does not document the specific algorithm used so results may differ. DuckDB does not
4691        support RESPECT NULLS.
4692        """
4693        this = expression.this
4694        if isinstance(this, exp.Distinct):
4695            # APPROX_QUANTILES requires 2 args and DISTINCT node grabs both
4696            if len(this.expressions) < 2:
4697                self.unsupported("APPROX_QUANTILES requires a bucket count argument")
4698                return self.function_fallback_sql(expression)
4699            num_quantiles_expr = this.expressions[1].pop()
4700        else:
4701            num_quantiles_expr = expression.expression
4702
4703        if not isinstance(num_quantiles_expr, exp.Literal) or not num_quantiles_expr.is_int:
4704            self.unsupported("APPROX_QUANTILES bucket count must be a positive integer")
4705            return self.function_fallback_sql(expression)
4706
4707        num_quantiles = t.cast(int, num_quantiles_expr.to_py())
4708        if num_quantiles <= 0:
4709            self.unsupported("APPROX_QUANTILES bucket count must be a positive integer")
4710            return self.function_fallback_sql(expression)
4711
4712        quantiles = [
4713            exp.Literal.number(Decimal(i) / Decimal(num_quantiles))
4714            for i in range(num_quantiles + 1)
4715        ]
4716
4717        return self.sql(exp.ApproxQuantile(this=this, quantile=exp.Array(expressions=quantiles)))
4718
4719    def jsonextractscalar_sql(self, expression: exp.JSONExtractScalar) -> str:
4720        if expression.args.get("scalar_only"):
4721            expression = exp.JSONExtractScalar(
4722                this=rename_func("JSON_VALUE")(self, expression), expression="'$'"
4723            )
4724        return _arrow_json_extract_sql(self, expression)
4725
4726    def bitwisenot_sql(self, expression: exp.BitwiseNot) -> str:
4727        this = expression.this
4728
4729        if _is_binary(this):
4730            expression.type = exp.DType.BINARY.into_expr()
4731
4732        arg = _cast_to_bit(this)
4733
4734        if isinstance(this, exp.Neg):
4735            arg = exp.Paren(this=arg)
4736
4737        expression.set("this", arg)
4738
4739        result_sql = f"~{self.sql(expression, 'this')}"
4740
4741        return _gen_with_cast_to_blob(self, expression, result_sql)
4742
4743    def window_sql(self, expression: exp.Window) -> str:
4744        this = expression.this
4745        if isinstance(this, exp.Corr) or (
4746            isinstance(this, exp.Filter) and isinstance(this.this, exp.Corr)
4747        ):
4748            return self._corr_sql(expression)
4749
4750        return super().window_sql(expression)
4751
4752    def filter_sql(self, expression: exp.Filter) -> str:
4753        if isinstance(expression.this, exp.Corr):
4754            return self._corr_sql(expression)
4755
4756        return super().filter_sql(expression)
4757
4758    def _corr_sql(
4759        self,
4760        expression: exp.Filter | exp.Window | exp.Corr,
4761    ) -> str:
4762        if isinstance(expression, exp.Corr) and not expression.args.get("null_on_zero_variance"):
4763            return self.func("CORR", expression.this, expression.expression)
4764
4765        corr_expr = _maybe_corr_null_to_false(expression)
4766        if corr_expr is None:
4767            if isinstance(expression, exp.Window):
4768                return super().window_sql(expression)
4769            if isinstance(expression, exp.Filter):
4770                return super().filter_sql(expression)
4771            corr_expr = expression  # make mypy happy
4772
4773        return self.sql(exp.case().when(exp.IsNan(this=corr_expr), exp.null()).else_(corr_expr))
4774
4775    def uuid_sql(self, expression: exp.Uuid) -> str:
4776        namespace = expression.this
4777        name = expression.args.get("name")
4778
4779        # UUID v5 (namespace + name) - Emulate using SHA1
4780        if namespace and name:
4781            result = exp.replace_placeholders(
4782                self.UUID_V5_TEMPLATE.copy(),
4783                namespace=namespace,
4784                name=name,
4785            )
4786            return self.sql(result)
4787
4788        return super().uuid_sql(expression)
TIMEZONE_PATTERN = re.compile(':\\d{2}.*?[+\\-]\\d{2}(?::\\d{2})?')
REGEX_ESCAPE_REPLACEMENTS = {'\\': '\\\\', '-': '\\-', '^': '\\^', '[': '\\[', ']': '\\]'}
RANDSTR_CHAR_POOL = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
RANDSTR_SEED = 123456
WS_CONTROL_CHARS_TO_DUCK = {'\x0b': 11, '\x1c': 28, '\x1d': 29, '\x1e': 30, '\x1f': 31}
MAX_BIT_POSITION = Literal(this=32768, is_string=False)
def connect_by_to_recursive_cte( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
710def connect_by_to_recursive_cte(expression: exp.Expr) -> exp.Expr:
711    # Rewrites START WITH ... CONNECT BY PRIOR into WITH RECURSIVE
712    # Falls through unchanged if there are no PRIORs.
713    if not isinstance(expression, exp.Select) or not expression.args.get("connect"):
714        return expression
715
716    connect = expression.args["connect"]
717    connect_pred = connect.args["connect"]
718
719    priors = list(connect_pred.find_all(exp.Prior))
720    if not priors:
721        return expression
722
723    from_ = expression.args.get("from_")
724    if not from_ or expression.args.get("joins"):
725        return expression
726
727    source_table = from_.this
728    base_select_exprs = expression.expressions
729    base_where = expression.args.get("where")
730    base_with = expression.args.get("with_")
731
732    # LEVEL is a Snowflake pseudo-column: it's always computed as a depth counter in the CTE.
733    has_level = any(
734        isinstance(col, exp.Column) and col.name.upper() == "LEVEL"
735        for e in base_select_exprs
736        for col in e.find_all(exp.Column)
737    )
738    has_star = expression.is_star
739
740    # CONNECT_BY_ROOT col yields the value of `col` from the START WITH row that begins each
741    # branch. Each one is threaded through the CTE as an extra column: the anchor binds it to the
742    # row's own value, the recursive arm forwards the parent's value unchanged.
743    root_col_names: list[str] = []
744    anchor_root_cols: list[exp.Expr] = []
745    inner_root_cols: list[exp.Expr] = []
746    roots = [root for e in base_select_exprs for root in e.find_all(exp.ConnectByRoot)]
747
748    for i, root in enumerate(roots):
749        name = f"_connect_by_root_{i}"
750        root_col_names.append(name)
751        anchor_root_cols.append(exp.alias_(root.this, name))
752        inner_root_cols.append(exp.alias_(exp.column(name, "_parent_row"), name))
753        root.replace(exp.column(name))
754
755    # Build the join condition from the full CONNECT BY predicate:
756    # PRIOR(col) → _parent_row.col, unqualified cols → _child_row.col.
757    def _qualify_connect_pred(node: exp.Expression) -> exp.Expression:
758        for col in find_all_in_scope(node, exp.Column):
759            col.set(
760                "table",
761                exp.to_identifier(
762                    "_parent_row" if isinstance(col.parent, exp.Prior) else "_child_row"
763                ),
764            )
765        for prior in find_all_in_scope(node, exp.Prior):
766            prior.replace(prior.this)
767        return node
768
769    # Avoid colliding with any CTE names already on the query.
770    cte_name = find_new_name(
771        {cte.alias for cte in (base_with.expressions if base_with else [])}, "_rootcte"
772    )
773
774    # Anchor: project all source columns + seed LEVEL at 1 + bind each root column to its own value.
775    anchor = exp.select(
776        exp.Star(), exp.alias_(exp.Literal.number(1), "level"), *anchor_root_cols
777    ).from_(source_table)
778    if connect.args.get("start"):
779        anchor = anchor.where(connect.args["start"])
780
781    # Recursive arm: carry all child columns + increment level + forward each root value.
782    # SELECT * in both arms means WHERE/PRIOR columns are always available without explicit tracking.
783    inner_query = (
784        exp.select(
785            exp.Column(this=exp.Star(), table=exp.to_identifier("_child_row")),
786            exp.alias_(exp.column("level", "_parent_row") + 1, "level"),
787            *inner_root_cols,
788        )
789        .from_(source_table.as_("_child_row"))
790        .join(exp.to_table(cte_name).as_("_parent_row"), on=_qualify_connect_pred(connect_pred))
791    )
792
793    # Outer SELECT re-projects from the CTE. Synthetic level/root columns are excluded from any
794    # star expansion (level only when not referenced) but kept where explicitly projected.
795    if has_star:
796        except_cols = [] if has_level else [exp.column("level")]
797        except_cols.extend(exp.column(name) for name in root_col_names)
798        star = exp.Star(except_=except_cols) if except_cols else exp.Star()
799        outer_select_exprs: list[exp.Expr] = [
800            star,
801            *(e for e in base_select_exprs if not e.is_star),
802        ]
803    else:
804        outer_select_exprs = base_select_exprs
805    outer_query = exp.select(*outer_select_exprs).from_(cte_name)
806    if base_where:
807        outer_query = outer_query.where(base_where.this)
808
809    # Attach the CTE, marking the WITH clause recursive.
810    if base_with:
811        outer_query.set("with_", base_with)
812    outer_query = outer_query.with_(
813        cte_name, as_=anchor.union(inner_query, distinct=False), recursive=True, copy=False
814    )
815
816    for arg, val in expression.args.items():
817        if val and arg not in _CONNECT_BY_ARGS_TO_SKIP:
818            outer_query.set(arg, val)
819
820    # Strip stale source table qualifiers in one pass; CTEs are child scopes so
821    # find_all_in_scope stays within the outer query only.
822    for col in find_all_in_scope(outer_query, exp.Column):
823        col.set("table", None)
824
825    return outer_query
WRAPPED_JSON_EXTRACT_EXPRESSIONS = (<class 'sqlglot.expressions.core.Binary'>, <class 'sqlglot.expressions.core.Bracket'>, <class 'sqlglot.expressions.core.In'>, <class 'sqlglot.expressions.core.Not'>)
class DuckDBGenerator(sqlglot.generator.Generator):
1554class DuckDBGenerator(generator.Generator):
1555    PARAMETER_TOKEN = "$"
1556    NAMED_PLACEHOLDER_TOKEN = "$"
1557    JOIN_HINTS = False
1558    TABLE_HINTS = False
1559    QUERY_HINTS = False
1560    LIMIT_FETCH = "LIMIT"
1561    STRUCT_DELIMITER = ("(", ")")
1562    RENAME_TABLE_WITH_DB = False
1563    NVL2_SUPPORTED = False
1564    SEMI_ANTI_JOIN_WITH_SIDE = False
1565    TABLESAMPLE_KEYWORDS = "USING SAMPLE"
1566    TABLESAMPLE_SEED_KEYWORD = "REPEATABLE"
1567    LAST_DAY_SUPPORTS_DATE_PART = False
1568    JSON_KEY_VALUE_PAIR_SEP = ","
1569    IGNORE_NULLS_IN_FUNC = True
1570    IGNORE_NULLS_BEFORE_ORDER = False
1571    JSON_PATH_BRACKETED_KEY_SUPPORTED = False
1572    SUPPORTS_CREATE_TABLE_LIKE = False
1573    MULTI_ARG_DISTINCT = False
1574    CAN_IMPLEMENT_ARRAY_ANY = True
1575    SUPPORTS_TO_NUMBER = False
1576    SELECT_KINDS: tuple[str, ...] = ()
1577    SUPPORTS_DECODE_CASE = False
1578    SUPPORTS_DROP_ALTER_ICEBERG_PROPERTY = False
1579
1580    AFTER_HAVING_MODIFIER_TRANSFORMS = generator.AFTER_HAVING_MODIFIER_TRANSFORMS
1581    SUPPORTS_WINDOW_EXCLUDE = True
1582    COPY_HAS_INTO_KEYWORD = False
1583    STAR_EXCEPT = "EXCLUDE"
1584    PAD_FILL_PATTERN_IS_REQUIRED = True
1585    ARRAY_SIZE_DIM_REQUIRED: bool | None = False
1586    NORMALIZE_EXTRACT_DATE_PARTS = True
1587    SUPPORTS_LIKE_QUANTIFIERS = False
1588    HISTORICAL_DATA_POST_ALIAS = True
1589    SET_ASSIGNMENT_REQUIRES_VARIABLE_KEYWORD = True
1590
1591    TRANSFORMS = {
1592        **generator.Generator.TRANSFORMS,
1593        exp.AnyValue: _anyvalue_sql,
1594        exp.ApproxDistinct: approx_count_distinct_sql,
1595        exp.Boolnot: _boolnot_sql,
1596        exp.Booland: _booland_sql,
1597        exp.Boolor: _boolor_sql,
1598        exp.Array: transforms.preprocess(
1599            [transforms.inherit_struct_field_names],
1600            generator=inline_array_unless_query,
1601        ),
1602        exp.ArrayAppend: array_append_sql("LIST_APPEND"),
1603        exp.ArrayCompact: array_compact_sql,
1604        exp.ArrayConstructCompact: lambda self, e: self.sql(
1605            exp.ArrayCompact(this=exp.Array(expressions=e.expressions))
1606        ),
1607        exp.ArrayConcat: array_concat_sql("LIST_CONCAT"),
1608        exp.ArrayContains: _array_contains_sql,
1609        exp.ArrayOverlaps: _array_overlaps_sql,
1610        exp.ArrayFilter: rename_func("LIST_FILTER"),
1611        exp.ArrayInsert: _array_insert_sql,
1612        exp.ArrayPosition: lambda self, e: (
1613            self.sql(
1614                exp.Sub(
1615                    this=exp.ArrayPosition(this=e.this, expression=e.expression),
1616                    expression=exp.Literal.number(1),
1617                )
1618            )
1619            if e.args.get("zero_based")
1620            else self.func("ARRAY_POSITION", e.this, e.expression)
1621        ),
1622        exp.ArrayRemoveAt: _array_remove_at_sql,
1623        exp.ArrayRemove: remove_from_array_using_filter,
1624        exp.ArraySort: _array_sort_sql,
1625        exp.ArrayPrepend: array_append_sql("LIST_PREPEND", swap_params=True),
1626        exp.ArraySum: rename_func("LIST_SUM"),
1627        exp.ArrayMax: rename_func("LIST_MAX"),
1628        exp.ArrayMin: rename_func("LIST_MIN"),
1629        exp.Base64DecodeBinary: lambda self, e: _base64_decode_sql(self, e, to_string=False),
1630        exp.Base64DecodeString: lambda self, e: _base64_decode_sql(self, e, to_string=True),
1631        exp.BitwiseAnd: lambda self, e: self._bitwise_op(e, "&"),
1632        exp.BitwiseAndAgg: _bitwise_agg_sql,
1633        exp.BitwiseCount: rename_func("BIT_COUNT"),
1634        exp.BitwiseLeftShift: _bitshift_sql,
1635        exp.BitwiseOr: lambda self, e: self._bitwise_op(e, "|"),
1636        exp.BitwiseOrAgg: _bitwise_agg_sql,
1637        exp.BitwiseRightShift: _bitshift_sql,
1638        exp.BitwiseXorAgg: _bitwise_agg_sql,
1639        exp.CommentColumnConstraint: no_comment_column_constraint_sql,
1640        exp.Corr: lambda self, e: self._corr_sql(e),
1641        exp.CosineDistance: rename_func("LIST_COSINE_DISTANCE"),
1642        exp.CurrentTime: lambda *_: "CURRENT_TIME",
1643        exp.CurrentSchemas: lambda self, e: self.func(
1644            "current_schemas", e.this if e.this else exp.true()
1645        ),
1646        exp.CurrentTimestamp: lambda self, e: (
1647            self.sql(
1648                exp.AtTimeZone(this=exp.var("CURRENT_TIMESTAMP"), zone=exp.Literal.string("UTC"))
1649            )
1650            if e.args.get("sysdate")
1651            else "CURRENT_TIMESTAMP"
1652        ),
1653        exp.CurrentVersion: rename_func("version"),
1654        exp.Localtime: unsupported_args("this")(lambda *_: "LOCALTIME"),
1655        exp.DayOfMonth: rename_func("DAYOFMONTH"),
1656        exp.DayOfWeek: rename_func("DAYOFWEEK"),
1657        exp.DayOfWeekIso: rename_func("ISODOW"),
1658        exp.DayOfYear: rename_func("DAYOFYEAR"),
1659        exp.Dayname: lambda self, e: (
1660            self.func("STRFTIME", e.this, exp.Literal.string("%a"))
1661            if e.args.get("abbreviated")
1662            else self.func("DAYNAME", e.this)
1663        ),
1664        exp.Monthname: lambda self, e: (
1665            self.func("STRFTIME", e.this, exp.Literal.string("%b"))
1666            if e.args.get("abbreviated")
1667            else self.func("MONTHNAME", e.this)
1668        ),
1669        exp.DataType: _datatype_sql,
1670        exp.Date: _date_sql,
1671        exp.DateAdd: _date_delta_to_binary_interval_op(),
1672        exp.DateFromParts: _date_from_parts_sql,
1673        exp.DateSub: _date_delta_to_binary_interval_op(),
1674        exp.DateDiff: _date_diff_sql,
1675        exp.DateStrToDate: datestrtodate_sql,
1676        exp.Datetime: no_datetime_sql,
1677        exp.DatetimeDiff: _date_diff_sql,
1678        exp.DatetimeSub: _date_delta_to_binary_interval_op(),
1679        exp.DatetimeAdd: _date_delta_to_binary_interval_op(),
1680        exp.DateToDi: lambda self, e: (
1681            f"CAST(STRFTIME({self.sql(e, 'this')}, {self.dialect.DATEINT_FORMAT}) AS INT)"
1682        ),
1683        exp.Decode: lambda self, e: encode_decode_sql(self, e, "DECODE", replace=False),
1684        exp.HexDecodeString: lambda self, e: self.sql(exp.Decode(this=exp.Unhex(this=e.this))),
1685        exp.DiToDate: lambda self, e: (
1686            f"CAST(STRPTIME(CAST({self.sql(e, 'this')} AS TEXT), {self.dialect.DATEINT_FORMAT}) AS DATE)"
1687        ),
1688        exp.Encode: lambda self, e: encode_decode_sql(self, e, "ENCODE", replace=False),
1689        exp.EqualNull: lambda self, e: self.sql(
1690            exp.NullSafeEQ(this=e.this, expression=e.expression)
1691        ),
1692        exp.EuclideanDistance: rename_func("LIST_DISTANCE"),
1693        exp.GenerateDateArray: _generate_datetime_array_sql,
1694        exp.GenerateSeries: generate_series_sql("GENERATE_SERIES", "RANGE"),
1695        exp.GenerateTimestampArray: _generate_datetime_array_sql,
1696        exp.Getbit: getbit_sql,
1697        exp.GroupConcat: lambda self, e: groupconcat_sql(self, e, within_group=False),
1698        exp.Explode: rename_func("UNNEST"),
1699        exp.IcebergProperty: lambda *_: "",
1700        exp.IntDiv: lambda self, e: self.binary(e, "//"),
1701        exp.IsInf: rename_func("ISINF"),
1702        exp.IsNan: rename_func("ISNAN"),
1703        exp.IsNullValue: lambda self, e: self.sql(
1704            exp.func("JSON_TYPE", e.this).eq(exp.Literal.string("NULL"))
1705        ),
1706        exp.IsArray: lambda self, e: self.sql(
1707            exp.func("JSON_TYPE", e.this).eq(exp.Literal.string("ARRAY"))
1708        ),
1709        exp.Ceil: _ceil_floor,
1710        exp.Floor: _ceil_floor,
1711        exp.JSONBExists: rename_func("JSON_EXISTS"),
1712        exp.JSONExtract: _arrow_json_extract_sql,
1713        exp.JSONExtractArray: _json_extract_value_array_sql,
1714        exp.JSONFormat: _json_format_sql,
1715        exp.JSONValueArray: _json_extract_value_array_sql,
1716        exp.Lateral: _explode_to_unnest_sql,
1717        exp.LogicalOr: lambda self, e: self.func("BOOL_OR", _cast_to_boolean(e.this)),
1718        exp.LogicalAnd: lambda self, e: self.func("BOOL_AND", _cast_to_boolean(e.this)),
1719        exp.Select: transforms.preprocess(
1720            [connect_by_to_recursive_cte, _seq_to_range_in_generator]
1721        ),
1722        exp.Seq1: lambda self, e: _seq_sql(self, e, 1),
1723        exp.Seq2: lambda self, e: _seq_sql(self, e, 2),
1724        exp.Seq4: lambda self, e: _seq_sql(self, e, 4),
1725        exp.Seq8: lambda self, e: _seq_sql(self, e, 8),
1726        exp.BoolxorAgg: _boolxor_agg_sql,
1727        exp.MakeInterval: lambda self, e: no_make_interval_sql(self, e, sep=" "),
1728        exp.Initcap: _initcap_sql,
1729        exp.MD5Digest: lambda self, e: self.func("UNHEX", self.func("MD5", e.this)),
1730        exp.SHA: lambda self, e: _sha_sql(self, e, "SHA1"),
1731        exp.SHA1Digest: lambda self, e: _sha_sql(self, e, "SHA1", is_binary=True),
1732        exp.SHA2: lambda self, e: _sha_sql(self, e, "SHA256"),
1733        exp.SHA2Digest: lambda self, e: _sha_sql(self, e, "SHA256", is_binary=True),
1734        exp.MonthsBetween: months_between_sql,
1735        exp.NextDay: _day_navigation_sql,
1736        exp.PercentileCont: rename_func("QUANTILE_CONT"),
1737        exp.PercentileDisc: rename_func("QUANTILE_DISC"),
1738        # DuckDB doesn't allow qualified columns inside of PIVOT expressions.
1739        # See: https://github.com/duckdb/duckdb/blob/671faf92411182f81dce42ac43de8bfb05d9909e/src/planner/binder/tableref/bind_pivot.cpp#L61-L62
1740        exp.Pivot: transforms.preprocess([transforms.unqualify_columns]),
1741        exp.PreviousDay: _day_navigation_sql,
1742        exp.RegexpILike: lambda self, e: self.func(
1743            "REGEXP_MATCHES", e.this, e.expression, exp.Literal.string("i")
1744        ),
1745        exp.RegexpSplit: rename_func("STR_SPLIT_REGEX"),
1746        exp.RegrValx: _regr_val_sql,
1747        exp.RegrValy: _regr_val_sql,
1748        exp.Return: lambda self, e: self.sql(e, "this"),
1749        exp.ReturnsProperty: lambda self, e: "TABLE" if isinstance(e.this, exp.Schema) else "",
1750        exp.StrToUnix: lambda self, e: self.func(
1751            "EPOCH", self.func("STRPTIME", e.this, self.format_time(e))
1752        ),
1753        exp.Struct: _struct_sql,
1754        exp.Transform: rename_func("LIST_TRANSFORM"),
1755        exp.TimeAdd: _date_delta_to_binary_interval_op(),
1756        exp.TimeSub: _date_delta_to_binary_interval_op(),
1757        exp.Time: no_time_sql,
1758        exp.TimeDiff: _timediff_sql,
1759        exp.Timestamp: no_timestamp_sql,
1760        exp.TimestampAdd: _date_delta_to_binary_interval_op(),
1761        exp.TimestampDiff: lambda self, e: self.func(
1762            "DATE_DIFF", exp.Literal.string(e.unit), e.expression, e.this
1763        ),
1764        exp.TimestampSub: _date_delta_to_binary_interval_op(),
1765        exp.TimeStrToDate: lambda self, e: self.sql(exp.cast(e.this, exp.DType.DATE)),
1766        exp.TimeStrToTime: timestrtotime_sql,
1767        exp.TimeStrToUnix: lambda self, e: self.func(
1768            "EPOCH", exp.cast(e.this, exp.DType.TIMESTAMP)
1769        ),
1770        exp.TimeToStr: lambda self, e: self.func("STRFTIME", e.this, self.format_time(e)),
1771        exp.ToBoolean: _to_boolean_sql,
1772        exp.ToVariant: lambda self, e: self.sql(
1773            exp.cast(e.this, exp.DataType.from_str("VARIANT", dialect="duckdb"))
1774        ),
1775        exp.TimeToUnix: rename_func("EPOCH"),
1776        exp.TsOrDiToDi: lambda self, e: (
1777            f"CAST(SUBSTR(REPLACE(CAST({self.sql(e, 'this')} AS TEXT), '-', ''), 1, 8) AS INT)"
1778        ),
1779        exp.TsOrDsAdd: _date_delta_to_binary_interval_op(),
1780        exp.TsOrDsDiff: lambda self, e: self.func(
1781            "DATE_DIFF",
1782            f"'{e.args.get('unit') or 'DAY'}'",
1783            exp.cast(e.expression, exp.DType.TIMESTAMP),
1784            exp.cast(e.this, exp.DType.TIMESTAMP),
1785        ),
1786        exp.UnixMicros: lambda self, e: self.func("EPOCH_US", _implicit_datetime_cast(e.this)),
1787        exp.UnixMillis: lambda self, e: self.func("EPOCH_MS", _implicit_datetime_cast(e.this)),
1788        exp.UnixSeconds: lambda self, e: self.sql(
1789            exp.cast(self.func("EPOCH", _implicit_datetime_cast(e.this)), exp.DType.BIGINT)
1790        ),
1791        exp.UnixToStr: lambda self, e: self.func(
1792            "STRFTIME", self.func("TO_TIMESTAMP", e.this), self.format_time(e)
1793        ),
1794        exp.UnixToTime: _unix_to_time_sql,
1795        exp.UnixToTimeStr: lambda self, e: f"CAST(TO_TIMESTAMP({self.sql(e, 'this')}) AS TEXT)",
1796        exp.VariancePop: rename_func("VAR_POP"),
1797        exp.WeekOfYear: rename_func("WEEKOFYEAR"),
1798        exp.YearOfWeek: lambda self, e: self.sql(
1799            exp.Extract(
1800                this=exp.Var(this="ISOYEAR"),
1801                expression=e.this,
1802            )
1803        ),
1804        exp.YearOfWeekIso: lambda self, e: self.sql(
1805            exp.Extract(
1806                this=exp.Var(this="ISOYEAR"),
1807                expression=e.this,
1808            )
1809        ),
1810        exp.Xor: _xor_sql,
1811        exp.JSONObjectAgg: rename_func("JSON_GROUP_OBJECT"),
1812        exp.JSONBObjectAgg: rename_func("JSON_GROUP_OBJECT"),
1813        exp.DateBin: rename_func("TIME_BUCKET"),
1814        exp.LastDay: _last_day_sql,
1815    }
1816
1817    SUPPORTED_JSON_PATH_PARTS = {
1818        exp.JSONPathKey,
1819        exp.JSONPathRoot,
1820        exp.JSONPathSubscript,
1821        exp.JSONPathWildcard,
1822    }
1823
1824    TYPE_MAPPING = {
1825        **generator.Generator.TYPE_MAPPING,
1826        exp.DType.BINARY: "BLOB",
1827        exp.DType.BPCHAR: "TEXT",
1828        exp.DType.CHAR: "TEXT",
1829        exp.DType.DATETIME: "TIMESTAMP",
1830        exp.DType.DECFLOAT: "DECIMAL",
1831        exp.DType.FLOAT: "REAL",
1832        exp.DType.JSONB: "JSON",
1833        exp.DType.NCHAR: "TEXT",
1834        exp.DType.NVARCHAR: "TEXT",
1835        exp.DType.UINT: "UINTEGER",
1836        exp.DType.VARBINARY: "BLOB",
1837        exp.DType.ROWVERSION: "BLOB",
1838        exp.DType.VARCHAR: "TEXT",
1839        exp.DType.TIMESTAMPLTZ: "TIMESTAMPTZ",
1840        exp.DType.TIMESTAMPNTZ: "TIMESTAMP",
1841        exp.DType.TIMESTAMP_S: "TIMESTAMP_S",
1842        exp.DType.TIMESTAMP_MS: "TIMESTAMP_MS",
1843        exp.DType.TIMESTAMP_NS: "TIMESTAMP_NS",
1844        exp.DType.BIGDECIMAL: "DECIMAL",
1845    }
1846
1847    TYPE_PARAM_SETTINGS = {
1848        **generator.Generator.TYPE_PARAM_SETTINGS,
1849        exp.DType.BIGDECIMAL: ((38, 5), (38, 38)),
1850        exp.DType.DECFLOAT: ((38, 5), (38, 38)),
1851    }
1852
1853    # https://github.com/duckdb/duckdb/blob/ff7f24fd8e3128d94371827523dae85ebaf58713/third_party/libpg_query/grammar/keywords/reserved_keywords.list#L1-L77
1854    RESERVED_KEYWORDS = {
1855        "array",
1856        "analyse",
1857        "union",
1858        "all",
1859        "when",
1860        "in_p",
1861        "default",
1862        "create_p",
1863        "window",
1864        "asymmetric",
1865        "to",
1866        "else",
1867        "localtime",
1868        "from",
1869        "end_p",
1870        "select",
1871        "current_date",
1872        "foreign",
1873        "with",
1874        "grant",
1875        "session_user",
1876        "or",
1877        "except",
1878        "references",
1879        "fetch",
1880        "limit",
1881        "group_p",
1882        "leading",
1883        "into",
1884        "collate",
1885        "offset",
1886        "do",
1887        "then",
1888        "localtimestamp",
1889        "check_p",
1890        "lateral_p",
1891        "current_role",
1892        "where",
1893        "asc_p",
1894        "placing",
1895        "desc_p",
1896        "user",
1897        "unique",
1898        "initially",
1899        "column",
1900        "both",
1901        "some",
1902        "as",
1903        "any",
1904        "only",
1905        "deferrable",
1906        "null_p",
1907        "current_time",
1908        "true_p",
1909        "table",
1910        "case",
1911        "trailing",
1912        "variadic",
1913        "for",
1914        "on",
1915        "distinct",
1916        "false_p",
1917        "not",
1918        "constraint",
1919        "current_timestamp",
1920        "returning",
1921        "primary",
1922        "intersect",
1923        "having",
1924        "analyze",
1925        "current_user",
1926        "and",
1927        "cast",
1928        "symmetric",
1929        "using",
1930        "order",
1931        "current_catalog",
1932    }
1933
1934    UNWRAPPED_INTERVAL_VALUES = (exp.Literal, exp.Paren)
1935
1936    # DuckDB doesn't generally support CREATE TABLE .. properties
1937    # https://duckdb.org/docs/sql/statements/create_table.html
1938    # There are a few exceptions (e.g. temporary tables) which are supported or
1939    # can be transpiled to DuckDB, so we explicitly override them accordingly
1940    PROPERTIES_LOCATION = {
1941        **{
1942            prop: exp.Properties.Location.UNSUPPORTED
1943            for prop in generator.Generator.PROPERTIES_LOCATION
1944        },
1945        exp.LikeProperty: exp.Properties.Location.POST_SCHEMA,
1946        exp.TemporaryProperty: exp.Properties.Location.POST_CREATE,
1947        exp.ReturnsProperty: exp.Properties.Location.POST_ALIAS,
1948        exp.SequenceProperties: exp.Properties.Location.POST_EXPRESSION,
1949        exp.IcebergProperty: exp.Properties.Location.POST_CREATE,
1950    }
1951
1952    IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS: t.ClassVar = _IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS
1953
1954    # Template for ZIPF transpilation - placeholders get replaced with actual parameters
1955    ZIPF_TEMPLATE: exp.Expr = exp.maybe_parse(
1956        """
1957        WITH rand AS (SELECT :random_expr AS r),
1958        weights AS (
1959            SELECT i, 1.0 / POWER(i, :s) AS w
1960            FROM RANGE(1, :n + 1) AS t(i)
1961        ),
1962        cdf AS (
1963            SELECT i, SUM(w) OVER (ORDER BY i) / SUM(w) OVER () AS p
1964            FROM weights
1965        )
1966        SELECT MIN(i)
1967        FROM cdf
1968        WHERE p >= (SELECT r FROM rand)
1969        """
1970    )
1971
1972    # Template for NORMAL transpilation using Box-Muller transform
1973    # mean + (stddev * sqrt(-2 * ln(u1)) * cos(2 * pi * u2))
1974    NORMAL_TEMPLATE: exp.Expr = exp.maybe_parse(
1975        ":mean + (:stddev * SQRT(-2 * LN(GREATEST(:u1, 1e-10))) * COS(2 * PI() * :u2))"
1976    )
1977
1978    # Template for generating a seeded pseudo-random value in [0, 1) from a hash
1979    SEEDED_RANDOM_TEMPLATE: exp.Expr = exp.maybe_parse("(ABS(HASH(:seed)) % 1000000) / 1000000.0")
1980
1981    # Template for generating signed and unsigned SEQ values within a specified range
1982    SEQ_UNSIGNED: exp.Expr = _SEQ_UNSIGNED
1983    SEQ_SIGNED: exp.Expr = _SEQ_SIGNED
1984
1985    # Template for MAP_CAT transpilation - Snowflake semantics:
1986    # 1. Returns NULL if either input is NULL
1987    # 2. For duplicate keys, prefers non-NULL value (COALESCE(m2[k], m1[k]))
1988    # 3. Filters out entries with NULL values from the result
1989    MAPCAT_TEMPLATE: exp.Expr = exp.maybe_parse(
1990        """
1991        CASE
1992            WHEN :map1 IS NULL OR :map2 IS NULL THEN NULL
1993            ELSE MAP_FROM_ENTRIES(LIST_FILTER(LIST_TRANSFORM(
1994                LIST_DISTINCT(LIST_CONCAT(MAP_KEYS(:map1), MAP_KEYS(:map2))),
1995                __k -> STRUCT_PACK(key := __k, value := COALESCE(:map2[__k], :map1[__k]))
1996            ), __x -> __x.value IS NOT NULL))
1997        END
1998        """
1999    )
2000
2001    # Mappings for EXTRACT/DATE_PART transpilation
2002    # Maps Snowflake specifiers unsupported in DuckDB to strftime format codes
2003    EXTRACT_STRFTIME_MAPPINGS: dict[str, tuple[str, str]] = {
2004        "WEEKISO": ("%V", "INTEGER"),
2005        "YEAROFWEEK": ("%G", "INTEGER"),
2006        "YEAROFWEEKISO": ("%G", "INTEGER"),
2007        "NANOSECOND": ("%n", "BIGINT"),
2008    }
2009
2010    # Maps epoch-based specifiers to DuckDB epoch functions
2011    EXTRACT_EPOCH_MAPPINGS: dict[str, str] = {
2012        "EPOCH_SECOND": "EPOCH",
2013        "EPOCH_MILLISECOND": "EPOCH_MS",
2014        "EPOCH_MICROSECOND": "EPOCH_US",
2015        "EPOCH_NANOSECOND": "EPOCH_NS",
2016    }
2017
2018    # Template for BITMAP_CONSTRUCT_AGG transpilation
2019    #
2020    # BACKGROUND:
2021    # Snowflake's BITMAP_CONSTRUCT_AGG aggregates integers into a compact binary bitmap.
2022    # Supports values in range 0-32767, this version returns NULL if any value is out of range
2023    # See: https://docs.snowflake.com/en/sql-reference/functions/bitmap_construct_agg
2024    # See: https://docs.snowflake.com/en/user-guide/querying-bitmaps-for-distinct-counts
2025    #
2026    # Snowflake uses two different formats based on the number of unique values:
2027    #
2028    # Format 1 - Small bitmap (< 5 unique values): Length of 10 bytes
2029    #   Bytes 0-1: Count of values as 2-byte big-endian integer (e.g., 3 values = 0x0003)
2030    #   Bytes 2-9: Up to 4 values, each as 2-byte little-endian integers, zero-padded to 8 bytes
2031    #   Example: Values [1, 2, 3] -> 0x0003 0100 0200 0300 0000 (hex)
2032    #                                count  v1   v2   v3   pad
2033    #
2034    # Format 2 - Large bitmap (>= 5 unique values): Length of 10 + (2 * count) bytes
2035    #   Bytes 0-9: Fixed header 0x08 followed by 9 zero bytes
2036    #   Bytes 10+: Each value as 2-byte little-endian integer (no padding)
2037    #   Example: Values [1,2,3,4,5] -> 0x08 00000000 00000000 00 0100 0200 0300 0400 0500
2038    #                                  hdr  ----9 zero bytes----  v1   v2   v3   v4   v5
2039    #
2040    # TEMPLATE STRUCTURE
2041    #
2042    # Phase 1 - Innermost subquery: Data preparation
2043    #   SELECT LIST_SORT(...) AS l
2044    #   - Aggregates all input values into a list, remove NULLs, duplicates and sorts
2045    #   Result: Clean, sorted list of unique non-null integers stored as 'l'
2046    #
2047    # Phase 2 - Middle subquery: Hex string construction
2048    #   LIST_TRANSFORM(...)
2049    #   - Converts each integer to 2-byte little-endian hex representation
2050    #   - & 255 extracts low byte, >> 8 extracts high byte
2051    #   - LIST_REDUCE: Concatenates all hex pairs into single string 'h'
2052    #   Result: Hex string of all values
2053    #
2054    # Phase 3 - Outer SELECT: Final bitmap assembly
2055    #   LENGTH(l) < 5:
2056    #   - Small format: 2-byte count (big-endian via %04X) + values + zero padding
2057    #   LENGTH(l) >= 5:
2058    #   - Large format: Fixed 10-byte header + values (no padding needed)
2059    #   Result: Complete binary bitmap as BLOB
2060    #
2061    BITMAP_CONSTRUCT_AGG_TEMPLATE: exp.Expr = exp.maybe_parse(
2062        """
2063        SELECT CASE
2064            WHEN l IS NULL OR LENGTH(l) = 0 THEN NULL
2065            WHEN LENGTH(l) != LENGTH(LIST_FILTER(l, __v -> __v BETWEEN 0 AND 32767)) THEN NULL
2066            WHEN LENGTH(l) < 5 THEN UNHEX(PRINTF('%04X', LENGTH(l)) || h || REPEAT('00', GREATEST(0, 4 - LENGTH(l)) * 2))
2067            ELSE UNHEX('08000000000000000000' || h)
2068        END
2069        FROM (
2070            SELECT l, COALESCE(LIST_REDUCE(
2071                LIST_TRANSFORM(l, __x -> PRINTF('%02X%02X', CAST(__x AS INT) & 255, (CAST(__x AS INT) >> 8) & 255)),
2072                (__a, __b) -> __a || __b, ''
2073            ), '') AS h
2074            FROM (SELECT LIST_SORT(LIST_DISTINCT(LIST(:arg) FILTER(NOT :arg IS NULL))) AS l)
2075        )
2076        """
2077    )
2078
2079    # Template for RANDSTR transpilation - placeholders get replaced with actual parameters
2080    RANDSTR_TEMPLATE: exp.Expr = exp.maybe_parse(
2081        f"""
2082        SELECT LISTAGG(
2083            SUBSTRING(
2084                '{RANDSTR_CHAR_POOL}',
2085                1 + CAST(FLOOR(random_value * 62) AS INT),
2086                1
2087            ),
2088            ''
2089        )
2090        FROM (
2091            SELECT (ABS(HASH(i + :seed)) % 1000) / 1000.0 AS random_value
2092            FROM RANGE(:length) AS t(i)
2093        )
2094        """,
2095    )
2096
2097    # Template for MINHASH transpilation
2098    # Computes k minimum hash values across aggregated data using DuckDB list functions
2099    # Returns JSON matching Snowflake format: {"state": [...], "type": "minhash", "version": 1}
2100    MINHASH_TEMPLATE: exp.Expr = exp.maybe_parse(
2101        """
2102        SELECT JSON_OBJECT('state', LIST(min_h ORDER BY seed), 'type', 'minhash', 'version', 1)
2103        FROM (
2104            SELECT seed, LIST_MIN(LIST_TRANSFORM(vals, __v -> HASH(CAST(__v AS VARCHAR) || CAST(seed AS VARCHAR)))) AS min_h
2105            FROM (SELECT LIST(:expr) AS vals), RANGE(0, :k) AS t(seed)
2106        )
2107        """,
2108    )
2109
2110    # Template for MINHASH_COMBINE transpilation
2111    # Combines multiple minhash signatures by taking element-wise minimum
2112    MINHASH_COMBINE_TEMPLATE: exp.Expr = exp.maybe_parse(
2113        """
2114        SELECT JSON_OBJECT('state', LIST(min_h ORDER BY idx), 'type', 'minhash', 'version', 1)
2115        FROM (
2116            SELECT
2117                pos AS idx,
2118                MIN(val) AS min_h
2119            FROM
2120                UNNEST(LIST(:expr)) AS _(sig),
2121                UNNEST(CAST(sig -> 'state' AS UBIGINT[])) WITH ORDINALITY AS t(val, pos)
2122            GROUP BY pos
2123        )
2124        """,
2125    )
2126
2127    # Template for APPROXIMATE_SIMILARITY transpilation
2128    # Computes multi-way Jaccard similarity: fraction of positions where ALL signatures agree
2129    APPROXIMATE_SIMILARITY_TEMPLATE: exp.Expr = exp.maybe_parse(
2130        """
2131        SELECT CAST(SUM(CASE WHEN num_distinct = 1 THEN 1 ELSE 0 END) AS DOUBLE) / COUNT(*)
2132        FROM (
2133            SELECT pos, COUNT(DISTINCT h) AS num_distinct
2134            FROM (
2135                SELECT h, pos
2136                FROM UNNEST(LIST(:expr)) AS _(sig),
2137                     UNNEST(CAST(sig -> 'state' AS UBIGINT[])) WITH ORDINALITY AS s(h, pos)
2138            )
2139            GROUP BY pos
2140        )
2141        """,
2142    )
2143
2144    # Template for ARRAYS_ZIP transpilation
2145    # Snowflake pads to longest array; DuckDB LIST_ZIP truncates to shortest
2146    # Uses RANGE + indexing to match Snowflake behavior
2147    ARRAYS_ZIP_TEMPLATE: exp.Expr = exp.maybe_parse(
2148        """
2149        CASE WHEN :null_check THEN NULL
2150        WHEN :all_empty_check THEN [:empty_struct]
2151        ELSE LIST_TRANSFORM(RANGE(0, :max_len), __i -> :transform_struct)
2152        END
2153        """,
2154    )
2155
2156    UUID_V5_TEMPLATE: exp.Expr = exp.maybe_parse(
2157        """
2158        (SELECT
2159            LOWER(
2160                SUBSTR(h, 1, 8) || '-' ||
2161                SUBSTR(h, 9, 4) || '-' ||
2162                '5' || SUBSTR(h, 14, 3) || '-' ||
2163                FORMAT('{:02x}', CAST('0x' || SUBSTR(h, 17, 2) AS INT) & 63 | 128) || SUBSTR(h, 19, 2) || '-' ||
2164                SUBSTR(h, 21, 12)
2165            )
2166        FROM (
2167            SELECT SUBSTR(SHA1(UNHEX(REPLACE(:namespace, '-', '')) || ENCODE(:name, 'utf8')), 1, 32) AS h
2168        ))
2169        """
2170    )
2171
2172    # Shared bag semantics outer frame for ARRAY_EXCEPT and ARRAY_INTERSECTION.
2173    # Each element is paired with its 1-based position via LIST_ZIP, then filtered
2174    # by a comparison operator (supplied via :cond) that determines the operation:
2175    #   EXCEPT (>):        keep the N-th occurrence only if N > count in arr2
2176    #                      e.g. [2,2,2] EXCEPT [2,2] -> [2]
2177    #   INTERSECTION (<=): keep the N-th occurrence only if N <= count in arr2
2178    #                      e.g. [2,2,2] INTERSECT [2,2] -> [2,2]
2179    # IS NOT DISTINCT FROM is used for NULL-safe element comparison.
2180    ARRAY_BAG_TEMPLATE: exp.Expr = exp.maybe_parse(
2181        """
2182        CASE
2183            WHEN :arr1 IS NULL OR :arr2 IS NULL THEN NULL
2184            ELSE LIST_TRANSFORM(
2185                LIST_FILTER(
2186                    LIST_ZIP(:arr1, GENERATE_SERIES(1, LEN(:arr1))),
2187                    pair -> :cond
2188                ),
2189                pair -> pair[0]
2190            )
2191        END
2192        """
2193    )
2194
2195    ARRAY_EXCEPT_CONDITION: exp.Expr = exp.maybe_parse(
2196        "LEN(LIST_FILTER(:arr1[1:pair[1]], e -> e IS NOT DISTINCT FROM pair[0]))"
2197        " > LEN(LIST_FILTER(:arr2, e -> e IS NOT DISTINCT FROM pair[0]))"
2198    )
2199
2200    ARRAY_INTERSECTION_CONDITION: exp.Expr = exp.maybe_parse(
2201        "LEN(LIST_FILTER(:arr1[1:pair[1]], e -> e IS NOT DISTINCT FROM pair[0]))"
2202        " <= LEN(LIST_FILTER(:arr2, e -> e IS NOT DISTINCT FROM pair[0]))"
2203    )
2204
2205    # Set semantics for ARRAY_EXCEPT. Deduplicates arr1 via LIST_DISTINCT, then
2206    # filters out any element that appears at least once in arr2.
2207    #   e.g. [1,1,2,3] EXCEPT [1] -> [2,3]
2208    # IS NOT DISTINCT FROM is used for NULL-safe element comparison.
2209    ARRAY_EXCEPT_SET_TEMPLATE: exp.Expr = exp.maybe_parse(
2210        """
2211        CASE
2212            WHEN :arr1 IS NULL OR :arr2 IS NULL THEN NULL
2213            ELSE LIST_FILTER(
2214                LIST_DISTINCT(:arr1),
2215                e -> LEN(LIST_FILTER(:arr2, x -> x IS NOT DISTINCT FROM e)) = 0
2216            )
2217        END
2218        """
2219    )
2220
2221    # BigQuery's `x IN UNNEST(arr)` NULL semantics:
2222    #   NULL IN UNNEST([1, 2])  -> NULL
2223    #   3 IN UNNEST([1, NULL])  -> NULL
2224    #   3 IN UNNEST([1, 2])     -> FALSE
2225    #   1 IN UNNEST(NULL)       -> FALSE (not NULL)
2226    #   1 IN UNNEST([])         -> FALSE
2227    # The default `IN (SELECT UNNEST(...))` rewrite creates a correlated subquery
2228    # that DuckDB rejects inside non-inner joins, so a CASE expression is used instead.
2229    IN_UNNEST_TEMPLATE: exp.Expr = exp.maybe_parse(
2230        """
2231        CASE
2232            WHEN :arr IS NULL OR ARRAY_LENGTH(:arr) = 0 THEN FALSE
2233            WHEN ARRAY_CONTAINS(:arr, :value) THEN TRUE
2234            WHEN :value IS NULL OR ARRAY_LENGTH(:arr) <> LIST_COUNT(:arr) THEN NULL
2235            ELSE FALSE
2236        END
2237        """
2238    )
2239
2240    STRTOK_TO_ARRAY_TEMPLATE: exp.Expr = exp.maybe_parse(
2241        """
2242        CASE WHEN :delimiter IS NULL THEN NULL
2243        ELSE LIST_FILTER(
2244            REGEXP_SPLIT_TO_ARRAY(:string, CASE WHEN :delimiter = '' THEN '.^' ELSE CONCAT('[', :escaped, ']') END),
2245            x -> NOT x = ''
2246        ) END
2247        """
2248    )
2249
2250    # Template for STRTOK function transpilation
2251    #
2252    # DuckDB itself doesn't have a strtok function. This handles the transpilation from Snowflake to DuckDB.
2253    # We may need to adjust this if we want to support transpilation from other dialects
2254    #
2255    # CASE
2256    #     -- Snowflake: empty delimiter + empty input string -> NULL
2257    #     WHEN delimiter = '' AND input_str = '' THEN NULL
2258    #
2259    #     -- Snowflake: empty delimiter + non-empty input string -> treats whole input as 1 token -> return input string if index is 1
2260    #     WHEN delimiter = '' AND index = 1 THEN input_str
2261    #
2262    #     -- Snowflake: empty delimiter + non-empty input string -> treats whole input as 1 token -> return NULL if index is not 1
2263    #     WHEN delimiter = '' THEN NULL
2264    #
2265    #     -- Snowflake: negative indices return NULL
2266    #     WHEN index < 0 THEN NULL
2267    #
2268    #     -- Snowflake: return NULL if any argument is NULL
2269    #     WHEN input_str IS NULL OR delimiter IS NULL OR index IS NULL THEN NULL
2270    #
2271    #
2272    #     ELSE LIST_FILTER(
2273    #         REGEXP_SPLIT_TO_ARRAY(
2274    #             input_str,
2275    #             CASE
2276    #                 -- if delimiter is '', we don't want to surround it with '[' and ']' as '[]' is invalid for DuckDB
2277    #                 WHEN delimiter = '' THEN ''
2278    #
2279    #                 -- handle problematic regex characters in delimiter with REGEXP_REPLACE
2280    #                 -- turn delimiter into a regex char set, otherwise DuckDB will match in order, which we don't want
2281    #                 ELSE '[' || REGEXP_REPLACE(delimiter, problematic_char_set, '\\\1', 'g') || ']'
2282    #             END
2283    #         ),
2284    #
2285    #         -- Snowflake: don't return empty strings
2286    #         x -> NOT x = ''
2287    #     )[index]
2288    # END
2289    STRTOK_TEMPLATE: exp.Expr = exp.maybe_parse(
2290        """
2291        CASE
2292            WHEN :delimiter = '' AND :string = '' THEN NULL
2293            WHEN :delimiter = '' AND :part_index = 1 THEN :string
2294            WHEN :delimiter = '' THEN NULL
2295            WHEN :part_index < 0 THEN NULL
2296            WHEN :string IS NULL OR :delimiter IS NULL OR :part_index IS NULL THEN NULL
2297            ELSE :base_func
2298        END
2299        """
2300    )
2301
2302    # Snowflake AUTO detects 3 DATE formats: YYYY-MM-DD (ISO-8601), MM/DD/YYYY, DD-MON-YYYY.
2303    # DuckDB TRY_CAST handles ISO-8601 natively. For the other two formats we use CONTAINS('/')
2304    # and REGEXP_MATCHES('[A-Za-z]') as heuristics — these correctly handle single-digit months
2305    # and days (e.g. 1/5/2020, 5-JAN-2020) where a positional char check would fail.
2306    # Ref: https://docs.snowflake.com/en/sql-reference/date-time-input-output#date-formats
2307    _TRYCAST_DATE_SLASH_FMT = "%m/%d/%Y"
2308    _TRYCAST_DATE_MON_FMT = "%d-%b-%Y"
2309
2310    def _array_bag_sql(self, condition: exp.Expr, arr1: exp.Expr, arr2: exp.Expr) -> str:
2311        cond = exp.Paren(this=exp.replace_placeholders(condition, arr1=arr1, arr2=arr2))
2312        return self.sql(
2313            exp.replace_placeholders(self.ARRAY_BAG_TEMPLATE, arr1=arr1, arr2=arr2, cond=cond)
2314        )
2315
2316    def timeslice_sql(self, expression: exp.TimeSlice) -> str:
2317        """
2318        Transform Snowflake's TIME_SLICE to DuckDB's time_bucket.
2319
2320        Snowflake: TIME_SLICE(date_expr, slice_length, 'UNIT' [, 'START'|'END'])
2321        DuckDB:    time_bucket(INTERVAL 'slice_length' UNIT, date_expr)
2322
2323        For 'END' kind, add the interval to get the end of the slice.
2324        For DATE type with 'END', cast result back to DATE to preserve type.
2325        """
2326        date_expr = expression.this
2327        slice_length = expression.expression
2328        unit = expression.unit
2329        kind = expression.text("kind").upper()
2330
2331        # Create INTERVAL expression: INTERVAL 'N' UNIT
2332        interval_expr = exp.Interval(this=slice_length, unit=unit)
2333
2334        # Create base time_bucket expression
2335        time_bucket_expr = exp.func("time_bucket", interval_expr, date_expr)
2336
2337        # Check if we need the end of the slice (default is start)
2338        if not kind == "END":
2339            # For 'START', return time_bucket directly
2340            return self.sql(time_bucket_expr)
2341
2342        # For 'END', add the interval to get end of slice
2343        add_expr = exp.Add(this=time_bucket_expr, expression=interval_expr.copy())
2344
2345        # If input is DATE type, cast result back to DATE to preserve type
2346        # DuckDB converts DATE to TIMESTAMP when adding intervals
2347        if date_expr.is_type(exp.DType.DATE):
2348            return self.sql(exp.cast(add_expr, exp.DType.DATE))
2349
2350        return self.sql(add_expr)
2351
2352    def bitmapbucketnumber_sql(self, expression: exp.BitmapBucketNumber) -> str:
2353        """
2354        Transpile BITMAP_BUCKET_NUMBER function from Snowflake to DuckDB equivalent.
2355
2356        Snowflake's BITMAP_BUCKET_NUMBER returns a 1-based bucket identifier where:
2357        - Each bucket covers 32,768 values
2358        - Bucket numbering starts at 1
2359        - Formula: ((value - 1) // 32768) + 1 for positive values
2360
2361        For non-positive values (0 and negative), we use value // 32768 to avoid
2362        producing bucket 0 or positive bucket IDs for negative inputs.
2363        """
2364        value = expression.this
2365
2366        positive_formula = ((value - 1) // 32768) + 1
2367        non_positive_formula = value // 32768
2368
2369        # CASE WHEN value > 0 THEN ((value - 1) // 32768) + 1 ELSE value // 32768 END
2370        case_expr = (
2371            exp.case()
2372            .when(exp.GT(this=value, expression=exp.Literal.number(0)), positive_formula)
2373            .else_(non_positive_formula)
2374        )
2375        return self.sql(case_expr)
2376
2377    def bitmapbitposition_sql(self, expression: exp.BitmapBitPosition) -> str:
2378        """
2379        Transpile Snowflake's BITMAP_BIT_POSITION to DuckDB CASE expression.
2380
2381        Snowflake's BITMAP_BIT_POSITION behavior:
2382        - For n <= 0: returns ABS(n) % 32768
2383        - For n > 0: returns (n - 1) % 32768 (maximum return value is 32767)
2384        """
2385        this = expression.this
2386
2387        return self.sql(
2388            exp.Mod(
2389                this=exp.Paren(
2390                    this=exp.If(
2391                        this=exp.GT(this=this, expression=exp.Literal.number(0)),
2392                        true=this - exp.Literal.number(1),
2393                        false=exp.Abs(this=this),
2394                    )
2395                ),
2396                expression=MAX_BIT_POSITION,
2397            )
2398        )
2399
2400    def bitmapconstructagg_sql(self, expression: exp.BitmapConstructAgg) -> str:
2401        """
2402        Transpile Snowflake's BITMAP_CONSTRUCT_AGG to DuckDB equivalent.
2403        Uses a pre-parsed template with placeholders replaced by expression nodes.
2404
2405        Snowflake bitmap format:
2406        - Small (< 5 unique values): 2-byte count (big-endian) + values (little-endian) + padding to 10 bytes
2407        - Large (>= 5 unique values): 10-byte header (0x08 + 9 zeros) + values (little-endian)
2408        """
2409        arg = expression.this
2410        return (
2411            f"({self.sql(exp.replace_placeholders(self.BITMAP_CONSTRUCT_AGG_TEMPLATE, arg=arg))})"
2412        )
2413
2414    def getignorecase_sql(self, expression: exp.GetIgnoreCase) -> str:
2415        self.unsupported("DuckDB does not support the GET_IGNORE_CASE() function")
2416        return self.function_fallback_sql(expression)
2417
2418    def compress_sql(self, expression: exp.Compress) -> str:
2419        self.unsupported("DuckDB does not support the COMPRESS() function")
2420        return self.function_fallback_sql(expression)
2421
2422    def encrypt_sql(self, expression: exp.Encrypt) -> str:
2423        self.unsupported("ENCRYPT is not supported in DuckDB")
2424        return self.function_fallback_sql(expression)
2425
2426    def decrypt_sql(self, expression: exp.Decrypt) -> str:
2427        func_name = "TRY_DECRYPT" if expression.args.get("safe") else "DECRYPT"
2428        self.unsupported(f"{func_name} is not supported in DuckDB")
2429        return self.function_fallback_sql(expression)
2430
2431    def decryptraw_sql(self, expression: exp.DecryptRaw) -> str:
2432        func_name = "TRY_DECRYPT_RAW" if expression.args.get("safe") else "DECRYPT_RAW"
2433        self.unsupported(f"{func_name} is not supported in DuckDB")
2434        return self.function_fallback_sql(expression)
2435
2436    def encryptraw_sql(self, expression: exp.EncryptRaw) -> str:
2437        self.unsupported("ENCRYPT_RAW is not supported in DuckDB")
2438        return self.function_fallback_sql(expression)
2439
2440    def parseurl_sql(self, expression: exp.ParseUrl) -> str:
2441        self.unsupported("PARSE_URL is not supported in DuckDB")
2442        return self.function_fallback_sql(expression)
2443
2444    def parseip_sql(self, expression: exp.ParseIp) -> str:
2445        self.unsupported("PARSE_IP is not supported in DuckDB")
2446        return self.function_fallback_sql(expression)
2447
2448    def decompressstring_sql(self, expression: exp.DecompressString) -> str:
2449        self.unsupported("DECOMPRESS_STRING is not supported in DuckDB")
2450        return self.function_fallback_sql(expression)
2451
2452    def decompressbinary_sql(self, expression: exp.DecompressBinary) -> str:
2453        self.unsupported("DECOMPRESS_BINARY is not supported in DuckDB")
2454        return self.function_fallback_sql(expression)
2455
2456    def jarowinklersimilarity_sql(self, expression: exp.JarowinklerSimilarity) -> str:
2457        this = expression.this
2458        expr = expression.expression
2459
2460        if expression.args.get("case_insensitive"):
2461            this = exp.Upper(this=this)
2462            expr = exp.Upper(this=expr)
2463
2464        result = exp.func("JARO_WINKLER_SIMILARITY", this, expr)
2465
2466        if expression.args.get("integer_scale"):
2467            result = exp.cast(result * 100, "INTEGER")
2468
2469        return self.sql(result)
2470
2471    def nthvalue_sql(self, expression: exp.NthValue) -> str:
2472        from_first = expression.args.get("from_first", True)
2473        if not from_first:
2474            self.unsupported("DuckDB's NTH_VALUE doesn't support starting from the end ")
2475
2476        return self.function_fallback_sql(expression)
2477
2478    def randstr_sql(self, expression: exp.Randstr) -> str:
2479        """
2480        Transpile Snowflake's RANDSTR to DuckDB equivalent using deterministic hash-based random.
2481        Uses a pre-parsed template with placeholders replaced by expression nodes.
2482
2483        RANDSTR(length, generator) generates a random string of specified length.
2484        - With numeric seed: Use HASH(i + seed) for deterministic output (same seed = same result)
2485        - With RANDOM(): Use RANDOM() in the hash for non-deterministic output
2486        - No generator: Use default seed value
2487        """
2488        length = expression.this
2489        generator = expression.args.get("generator")
2490
2491        if generator:
2492            if isinstance(generator, exp.Rand):
2493                # If it's RANDOM(), use its seed if available, otherwise use RANDOM() itself
2494                seed_value = generator.this or generator
2495            else:
2496                # Const/int or other expression - use as seed directly
2497                seed_value = generator
2498        else:
2499            # No generator specified, use default seed (arbitrary but deterministic)
2500            seed_value = exp.Literal.number(RANDSTR_SEED)
2501
2502        replacements = {"seed": seed_value, "length": length}
2503        return f"({self.sql(exp.replace_placeholders(self.RANDSTR_TEMPLATE, **replacements))})"
2504
2505    @unsupported_args("finish")
2506    def reduce_sql(self, expression: exp.Reduce) -> str:
2507        array_arg = expression.this
2508        initial_value = expression.args.get("initial")
2509        merge_lambda = expression.args.get("merge")
2510
2511        if merge_lambda:
2512            merge_lambda.set("colon", True)
2513
2514        return self.func("list_reduce", array_arg, merge_lambda, initial_value)
2515
2516    def zipf_sql(self, expression: exp.Zipf) -> str:
2517        """
2518        Transpile Snowflake's ZIPF to DuckDB using CDF-based inverse sampling.
2519        Uses a pre-parsed template with placeholders replaced by expression nodes.
2520        """
2521        s = expression.this
2522        n = expression.args["elementcount"]
2523        gen = expression.args["gen"]
2524
2525        if not isinstance(gen, exp.Rand):
2526            # (ABS(HASH(seed)) % 1000000) / 1000000.0
2527            random_expr: exp.Expr = exp.Div(
2528                this=exp.Paren(
2529                    this=exp.Mod(
2530                        this=exp.Abs(this=exp.Anonymous(this="HASH", expressions=[gen.copy()])),
2531                        expression=exp.Literal.number(1000000),
2532                    )
2533                ),
2534                expression=exp.Literal.number(1000000.0),
2535            )
2536        else:
2537            # Use RANDOM() for non-deterministic output
2538            random_expr = exp.Rand()
2539
2540        replacements = {"s": s, "n": n, "random_expr": random_expr}
2541        return f"({self.sql(exp.replace_placeholders(self.ZIPF_TEMPLATE, **replacements))})"
2542
2543    def tobinary_sql(self, expression: exp.ToBinary) -> str:
2544        """
2545        TO_BINARY and TRY_TO_BINARY transpilation:
2546        - 'HEX': TO_BINARY('48454C50', 'HEX') -> UNHEX('48454C50')
2547        - 'UTF-8': TO_BINARY('TEST', 'UTF-8') -> ENCODE('TEST')
2548        - 'BASE64': TO_BINARY('SEVMUA==', 'BASE64') -> FROM_BASE64('SEVMUA==')
2549
2550        For TRY_TO_BINARY (safe=True), wrap with TRY():
2551        - 'HEX': TRY_TO_BINARY('invalid', 'HEX') -> TRY(UNHEX('invalid'))
2552        """
2553        value = expression.this
2554        format_arg = expression.args.get("format")
2555        is_safe = expression.args.get("safe")
2556        is_binary = _is_binary(expression)
2557
2558        if not format_arg and not is_binary:
2559            func_name = "TRY_TO_BINARY" if is_safe else "TO_BINARY"
2560            return self.func(func_name, value)
2561
2562        # Snowflake defaults to HEX encoding when no format is specified
2563        fmt = format_arg.name.upper() if format_arg else "HEX"
2564
2565        if fmt in ("UTF-8", "UTF8"):
2566            # DuckDB ENCODE always uses UTF-8, no charset parameter needed
2567            result = self.func("ENCODE", value)
2568        elif fmt == "BASE64":
2569            result = self.func("FROM_BASE64", value)
2570        elif fmt == "HEX":
2571            result = self.func("UNHEX", value)
2572        else:
2573            if is_safe:
2574                return self.sql(exp.null())
2575            else:
2576                self.unsupported(f"format {fmt} is not supported")
2577                result = self.func("TO_BINARY", value)
2578        return f"TRY({result})" if is_safe else result
2579
2580    def tonumber_sql(self, expression: exp.ToNumber) -> str:
2581        fmt = expression.args.get("format")
2582        precision = expression.args.get("precision")
2583        scale = expression.args.get("scale")
2584
2585        if not fmt and precision and scale:
2586            return self.sql(
2587                exp.cast(
2588                    expression.this, f"DECIMAL({precision.name}, {scale.name})", dialect="duckdb"
2589                )
2590            )
2591
2592        return super().tonumber_sql(expression)
2593
2594    def _greatest_least_sql(self, expression: exp.Greatest | exp.Least) -> str:
2595        """
2596        Handle GREATEST/LEAST functions with dialect-aware NULL behavior.
2597
2598        - If ignore_nulls=False (BigQuery-style): return NULL if any argument is NULL
2599        - If ignore_nulls=True (DuckDB/PostgreSQL-style): ignore NULLs, return greatest/least non-NULL value
2600        """
2601        # Get all arguments
2602        all_args = [expression.this, *expression.expressions]
2603        fallback_sql = self.function_fallback_sql(expression)
2604
2605        if expression.args.get("ignore_nulls"):
2606            # DuckDB/PostgreSQL behavior: use native GREATEST/LEAST (ignores NULLs)
2607            return self.sql(fallback_sql)
2608
2609        # return NULL if any argument is NULL
2610        case_expr = exp.case().when(
2611            exp.or_(*[arg.is_(exp.null()) for arg in all_args], copy=False),
2612            exp.null(),
2613            copy=False,
2614        )
2615        case_expr.set("default", fallback_sql)
2616        return self.sql(case_expr)
2617
2618    def generator_sql(self, expression: exp.Generator) -> str:
2619        # Transpile Snowflake GENERATOR to DuckDB range()
2620        rowcount = expression.args.get("rowcount")
2621        time_limit = expression.args.get("time_limit")
2622
2623        if time_limit:
2624            self.unsupported("GENERATOR TIMELIMIT parameter is not supported in DuckDB")
2625
2626        if not rowcount:
2627            self.unsupported("GENERATOR without ROWCOUNT is not supported in DuckDB")
2628            return self.func("range", exp.Literal.number(0))
2629
2630        return self.func("range", rowcount)
2631
2632    def greatest_sql(self, expression: exp.Greatest) -> str:
2633        return self._greatest_least_sql(expression)
2634
2635    def least_sql(self, expression: exp.Least) -> str:
2636        return self._greatest_least_sql(expression)
2637
2638    def lambda_sql(self, expression: exp.Lambda, arrow_sep: str = "->", wrap: bool = True) -> str:
2639        if expression.args.get("colon"):
2640            prefix = "LAMBDA "
2641            arrow_sep = ":"
2642            wrap = False
2643        else:
2644            prefix = ""
2645
2646        lambda_sql = super().lambda_sql(expression, arrow_sep=arrow_sep, wrap=wrap)
2647        return f"{prefix}{lambda_sql}"
2648
2649    def show_sql(self, expression: exp.Show) -> str:
2650        from_ = self.sql(expression, "from_")
2651        from_ = f" FROM {from_}" if from_ else ""
2652        return f"SHOW {expression.name}{from_}"
2653
2654    def soundex_sql(self, expression: exp.Soundex) -> str:
2655        self.unsupported("SOUNDEX is not supported in DuckDB")
2656        return self.func("SOUNDEX", expression.this)
2657
2658    def sortarray_sql(self, expression: exp.SortArray) -> str:
2659        arr = expression.this
2660        asc = expression.args.get("asc")
2661        nulls_first = expression.args.get("nulls_first")
2662
2663        if not isinstance(asc, exp.Boolean) and not isinstance(nulls_first, exp.Boolean):
2664            return self.func("LIST_SORT", arr, asc, nulls_first)
2665
2666        nulls_are_first = nulls_first == exp.true()
2667        nulls_first_sql = exp.Literal.string("NULLS FIRST") if nulls_are_first else None
2668
2669        if not isinstance(asc, exp.Boolean):
2670            return self.func("LIST_SORT", arr, asc, nulls_first_sql)
2671
2672        descending = asc == exp.false()
2673
2674        if not descending and not nulls_are_first:
2675            return self.func("LIST_SORT", arr)
2676        if not nulls_are_first:
2677            return self.func("ARRAY_REVERSE_SORT", arr)
2678        return self.func(
2679            "LIST_SORT",
2680            arr,
2681            exp.Literal.string("DESC" if descending else "ASC"),
2682            exp.Literal.string("NULLS FIRST"),
2683        )
2684
2685    def install_sql(self, expression: exp.Install) -> str:
2686        force = "FORCE " if expression.args.get("force") else ""
2687        this = self.sql(expression, "this")
2688        from_clause = expression.args.get("from_")
2689        from_clause = f" FROM {from_clause}" if from_clause else ""
2690        return f"{force}INSTALL {this}{from_clause}"
2691
2692    def approxtopk_sql(self, expression: exp.ApproxTopK) -> str:
2693        self.unsupported(
2694            "APPROX_TOP_K cannot be transpiled to DuckDB due to incompatible return types. "
2695        )
2696        return self.function_fallback_sql(expression)
2697
2698    def strposition_sql(self, expression: exp.StrPosition) -> str:
2699        this = expression.this
2700        substr = expression.args.get("substr")
2701        position = expression.args.get("position")
2702
2703        # For BINARY/BLOB: DuckDB's STRPOS doesn't support BLOB types
2704        # Convert to HEX strings, use STRPOS, then convert hex position to byte position
2705        if _is_binary(this):
2706            # Build expression: STRPOS(HEX(haystack), HEX(needle))
2707            hex_strpos = exp.StrPosition(
2708                this=exp.Hex(this=this),
2709                substr=exp.Hex(this=substr),
2710            )
2711
2712            return self.sql(exp.cast((hex_strpos + 1) / 2, exp.DType.INT))
2713
2714        # For VARCHAR: handle clamp_position
2715        if expression.args.get("clamp_position") and position:
2716            expression = expression.copy()
2717            expression.set(
2718                "position",
2719                exp.If(
2720                    this=exp.LTE(this=position, expression=exp.Literal.number(0)),
2721                    true=exp.Literal.number(1),
2722                    false=position.copy(),
2723                ),
2724            )
2725
2726        return strposition_sql(self, expression)
2727
2728    def substring_sql(self, expression: exp.Substring) -> str:
2729        if expression.args.get("zero_start"):
2730            start = expression.args.get("start")
2731            length = expression.args.get("length")
2732
2733            if start := expression.args.get("start"):
2734                start = exp.If(this=start.eq(0), true=exp.Literal.number(1), false=start)
2735            if length := expression.args.get("length"):
2736                length = exp.If(this=length < 0, true=exp.Literal.number(0), false=length)
2737
2738            return self.func("SUBSTRING", expression.this, start, length)
2739
2740        return self.function_fallback_sql(expression)
2741
2742    def strtotime_sql(self, expression: exp.StrToTime) -> str:
2743        # Check if target_type requires TIMESTAMPTZ (for LTZ/TZ variants)
2744        target_type = expression.args.get("target_type")
2745        needs_tz = target_type and target_type.this in (
2746            exp.DType.TIMESTAMPLTZ,
2747            exp.DType.TIMESTAMPTZ,
2748        )
2749
2750        value, formatted_time = self._strptime_default_year(expression)
2751
2752        if expression.args.get("safe"):
2753            cast_type = exp.DType.TIMESTAMPTZ if needs_tz else exp.DType.TIMESTAMP
2754            return self.sql(exp.cast(self.func("TRY_STRPTIME", value, formatted_time), cast_type))
2755
2756        base_sql = self.func("STRPTIME", value, formatted_time)
2757        if needs_tz:
2758            return self.sql(
2759                exp.cast(
2760                    base_sql,
2761                    exp.DataType(this=exp.DType.TIMESTAMPTZ),
2762                )
2763            )
2764        return base_sql
2765
2766    def strtodate_sql(self, expression: exp.StrToDate) -> str:
2767        value, formatted_time = self._strptime_default_year(expression)
2768        function_name = "STRPTIME" if not expression.args.get("safe") else "TRY_STRPTIME"
2769        return self.sql(
2770            exp.cast(
2771                self.func(function_name, value, formatted_time),
2772                exp.DataType(this=exp.DType.DATE),
2773            )
2774        )
2775
2776    def _strptime_default_year(
2777        self, expression: exp.StrToTime | exp.StrToDate | exp.ParseDatetime
2778    ) -> tuple[exp.ExpOrStr, exp.ExpOrStr | None]:
2779        value: exp.ExpOrStr = expression.this
2780        formatted_time: exp.ExpOrStr | None = self.format_time(expression)
2781
2782        if default_year := expression.args.get("default_year"):
2783            value = exp.DPipe(this=exp.Literal.string(f"{default_year.name} "), expression=value)
2784            formatted_time = exp.DPipe(this=exp.Literal.string("%Y "), expression=formatted_time)
2785
2786        return value, formatted_time
2787
2788    def parsedatetime_sql(self, expression: exp.ParseDatetime) -> str:
2789        value, formatted_time = self._strptime_default_year(expression)
2790        return self.func("STRPTIME", value, formatted_time)
2791
2792    def parsetime_sql(self, expression: exp.ParseTime) -> str:
2793        formatted_time = self.format_time(expression)
2794        return self.sql(
2795            exp.cast(
2796                self.func("STRPTIME", expression.this, formatted_time),
2797                exp.DataType(this=exp.DType.TIME),
2798            )
2799        )
2800
2801    def tsordstotime_sql(self, expression: exp.TsOrDsToTime) -> str:
2802        this = expression.this
2803        time_format = self.format_time(expression)
2804        safe = expression.args.get("safe")
2805        time_type = exp.DataType.from_str("TIME", dialect="duckdb")
2806        cast_expr = exp.TryCast if safe else exp.Cast
2807
2808        if time_format:
2809            func_name = "TRY_STRPTIME" if safe else "STRPTIME"
2810            strptime = exp.Anonymous(this=func_name, expressions=[this, time_format])
2811            return self.sql(cast_expr(this=strptime, to=time_type))
2812
2813        if isinstance(this, exp.TsOrDsToTime) or this.is_type(exp.DType.TIME):
2814            return self.sql(this)
2815
2816        return self.sql(cast_expr(this=this, to=time_type))
2817
2818    def currentdate_sql(self, expression: exp.CurrentDate) -> str:
2819        if not expression.this:
2820            return "CURRENT_DATE"
2821
2822        expr = exp.Cast(
2823            this=exp.AtTimeZone(this=exp.CurrentTimestamp(), zone=expression.this),
2824            to=exp.DataType(this=exp.DType.DATE),
2825        )
2826        return self.sql(expr)
2827
2828    def checkjson_sql(self, expression: exp.CheckJson) -> str:
2829        arg = expression.this
2830        return self.sql(
2831            exp.case()
2832            .when(
2833                exp.or_(arg.is_(exp.Null()), arg.eq(""), exp.func("json_valid", arg)),
2834                exp.null(),
2835            )
2836            .else_(exp.Literal.string("Invalid JSON"))
2837        )
2838
2839    def parsejson_sql(self, expression: exp.ParseJSON) -> str:
2840        arg = expression.this
2841        if expression.args.get("safe"):
2842            return self.sql(
2843                exp.case()
2844                .when(exp.func("json_valid", arg), exp.cast(arg.copy(), "JSON"))
2845                .else_(exp.null())
2846            )
2847        return self.func("JSON", arg)
2848
2849    def unicode_sql(self, expression: exp.Unicode) -> str:
2850        if expression.args.get("empty_is_zero"):
2851            return self.sql(
2852                exp.case()
2853                .when(expression.this.eq(exp.Literal.string("")), exp.Literal.number(0))
2854                .else_(exp.Anonymous(this="UNICODE", expressions=[expression.this]))
2855            )
2856
2857        return self.func("UNICODE", expression.this)
2858
2859    def stripnullvalue_sql(self, expression: exp.StripNullValue) -> str:
2860        return self.sql(
2861            exp.case()
2862            .when(exp.func("json_type", expression.this).eq("NULL"), exp.null())
2863            .else_(expression.this)
2864        )
2865
2866    def trunc_sql(self, expression: exp.Trunc) -> str:
2867        decimals = expression.args.get("decimals")
2868        if (
2869            expression.args.get("fractions_supported")
2870            and decimals
2871            and not decimals.is_type(exp.DType.INT)
2872        ):
2873            decimals = exp.cast(decimals, exp.DType.INT, dialect="duckdb")
2874
2875        return self.func("TRUNC", expression.this, decimals)
2876
2877    def normal_sql(self, expression: exp.Normal) -> str:
2878        """
2879        Transpile Snowflake's NORMAL(mean, stddev, gen) to DuckDB.
2880
2881        Uses the Box-Muller transform via NORMAL_TEMPLATE.
2882        """
2883        mean = expression.this
2884        stddev = expression.args["stddev"]
2885        gen: exp.Expr = expression.args["gen"]
2886
2887        # Build two uniform random values [0, 1) for Box-Muller transform
2888        if isinstance(gen, exp.Rand) and gen.this is None:
2889            u1: exp.Expr = exp.Rand()
2890            u2: exp.Expr = exp.Rand()
2891        else:
2892            # Seeded: derive two values using HASH with different inputs
2893            seed = gen.this if isinstance(gen, exp.Rand) else gen
2894            u1 = exp.replace_placeholders(self.SEEDED_RANDOM_TEMPLATE, seed=seed)
2895            u2 = exp.replace_placeholders(
2896                self.SEEDED_RANDOM_TEMPLATE,
2897                seed=exp.Add(this=seed.copy(), expression=exp.Literal.number(1)),
2898            )
2899
2900        replacements = {"mean": mean, "stddev": stddev, "u1": u1, "u2": u2}
2901        return self.sql(exp.replace_placeholders(self.NORMAL_TEMPLATE, **replacements))
2902
2903    def uniform_sql(self, expression: exp.Uniform) -> str:
2904        """
2905        Transpile Snowflake's UNIFORM(min, max, gen) to DuckDB.
2906
2907        UNIFORM returns a random value in [min, max]:
2908        - Integer result if both min and max are integers
2909        - Float result if either min or max is a float
2910        """
2911        min_val = expression.this
2912        max_val = expression.expression
2913        gen = expression.args.get("gen")
2914
2915        # Determine if result should be integer (both bounds are integers).
2916        # We do this to emulate Snowflake's behavior, INT -> INT, FLOAT -> FLOAT
2917        is_int_result = min_val.is_int and max_val.is_int
2918
2919        # Build the random value expression [0, 1)
2920        if not isinstance(gen, exp.Rand):
2921            # Seed value: (ABS(HASH(seed)) % 1000000) / 1000000.0
2922            random_expr: exp.Expr = exp.Div(
2923                this=exp.Paren(
2924                    this=exp.Mod(
2925                        this=exp.Abs(this=exp.Anonymous(this="HASH", expressions=[gen])),
2926                        expression=exp.Literal.number(1000000),
2927                    )
2928                ),
2929                expression=exp.Literal.number(1000000.0),
2930            )
2931        else:
2932            random_expr = exp.Rand()
2933
2934        # Build: min + random * (max - min [+ 1 for int])
2935        range_expr: exp.Expr = exp.Sub(this=max_val, expression=min_val)
2936        if is_int_result:
2937            range_expr = exp.Add(this=range_expr, expression=exp.Literal.number(1))
2938
2939        result: exp.Expr = exp.Add(
2940            this=min_val,
2941            expression=exp.Mul(this=random_expr, expression=exp.Paren(this=range_expr)),
2942        )
2943
2944        if is_int_result:
2945            result = exp.Cast(this=exp.Floor(this=result), to=exp.DType.BIGINT.into_expr())
2946
2947        return self.sql(result)
2948
2949    def timefromparts_sql(self, expression: exp.TimeFromParts) -> str:
2950        nano = expression.args.get("nano")
2951        overflow = expression.args.get("overflow")
2952
2953        # Snowflake's TIME_FROM_PARTS supports overflow
2954        if overflow:
2955            hour = expression.args["hour"]
2956            minute = expression.args["min"]
2957            sec = expression.args["sec"]
2958
2959            # Check if values are within normal ranges - use MAKE_TIME for efficiency
2960            if not nano and all(arg.is_int for arg in [hour, minute, sec]):
2961                try:
2962                    h_val = hour.to_py()
2963                    m_val = minute.to_py()
2964                    s_val = sec.to_py()
2965                    if 0 <= h_val <= 23 and 0 <= m_val <= 59 and 0 <= s_val <= 59:
2966                        return rename_func("MAKE_TIME")(self, expression)
2967                except ValueError:
2968                    pass
2969
2970            # Overflow or nanoseconds detected - use INTERVAL arithmetic
2971            if nano:
2972                sec = sec + nano.pop() / exp.Literal.number(1000000000.0)
2973
2974            total_seconds = hour * exp.Literal.number(3600) + minute * exp.Literal.number(60) + sec
2975
2976            return self.sql(
2977                exp.Add(
2978                    this=exp.Cast(
2979                        this=exp.Literal.string("00:00:00"), to=exp.DType.TIME.into_expr()
2980                    ),
2981                    expression=exp.Interval(this=total_seconds, unit=exp.var("SECOND")),
2982                )
2983            )
2984
2985        # Default: MAKE_TIME
2986        if nano:
2987            expression.set(
2988                "sec", expression.args["sec"] + nano.pop() / exp.Literal.number(1000000000.0)
2989            )
2990
2991        return rename_func("MAKE_TIME")(self, expression)
2992
2993    def extract_sql(self, expression: exp.Extract) -> str:
2994        """
2995        Transpile EXTRACT/DATE_PART for DuckDB, handling specifiers not natively supported.
2996
2997        DuckDB doesn't support: WEEKISO, YEAROFWEEK, YEAROFWEEKISO, NANOSECOND,
2998        EPOCH_SECOND (as integer), EPOCH_MILLISECOND, EPOCH_MICROSECOND, EPOCH_NANOSECOND
2999        """
3000        this = expression.this
3001        datetime_expr = expression.expression
3002
3003        # TIMESTAMPTZ extractions may produce different results between Snowflake and DuckDB
3004        # because Snowflake applies server timezone while DuckDB uses local timezone
3005        if datetime_expr.is_type(exp.DType.TIMESTAMPTZ, exp.DType.TIMESTAMPLTZ):
3006            self.unsupported(
3007                "EXTRACT from TIMESTAMPTZ / TIMESTAMPLTZ may produce different results due to timezone handling differences"
3008            )
3009
3010        part_name = this.name.upper()
3011
3012        if part_name in self.EXTRACT_STRFTIME_MAPPINGS:
3013            fmt, cast_type = self.EXTRACT_STRFTIME_MAPPINGS[part_name]
3014
3015            # Problem: strftime doesn't accept TIME and there's no NANOSECOND function
3016            # So, for NANOSECOND with TIME, fallback to MICROSECOND * 1000
3017            is_nano_time = part_name == "NANOSECOND" and datetime_expr.is_type(
3018                exp.DType.TIME, exp.DType.TIMETZ
3019            )
3020
3021            if is_nano_time:
3022                self.unsupported("Parameter NANOSECOND is not supported with TIME type in DuckDB")
3023                return self.sql(
3024                    exp.cast(
3025                        exp.Mul(
3026                            this=exp.Extract(this=exp.var("MICROSECOND"), expression=datetime_expr),
3027                            expression=exp.Literal.number(1000),
3028                        ),
3029                        exp.DataType.from_str(cast_type, dialect="duckdb"),
3030                    )
3031                )
3032
3033            # For NANOSECOND, cast to TIMESTAMP_NS to preserve nanosecond precision
3034            strftime_input = datetime_expr
3035            if part_name == "NANOSECOND":
3036                strftime_input = exp.cast(datetime_expr, exp.DType.TIMESTAMP_NS)
3037
3038            return self.sql(
3039                exp.cast(
3040                    exp.Anonymous(
3041                        this="STRFTIME",
3042                        expressions=[strftime_input, exp.Literal.string(fmt)],
3043                    ),
3044                    exp.DataType.from_str(cast_type, dialect="duckdb"),
3045                )
3046            )
3047
3048        if part_name in self.EXTRACT_EPOCH_MAPPINGS:
3049            func_name = self.EXTRACT_EPOCH_MAPPINGS[part_name]
3050            result: exp.Expr = exp.Anonymous(this=func_name, expressions=[datetime_expr])
3051            # EPOCH returns float, cast to BIGINT for integer result
3052            if part_name == "EPOCH_SECOND":
3053                result = exp.cast(result, exp.DataType.from_str("BIGINT", dialect="duckdb"))
3054            return self.sql(result)
3055
3056        return super().extract_sql(expression)
3057
3058    def timestampfromparts_sql(self, expression: exp.TimestampFromParts) -> str:
3059        # Check if this is the date/time expression form: TIMESTAMP_FROM_PARTS(date_expr, time_expr)
3060        date_expr = expression.this
3061        time_expr = expression.expression
3062
3063        if date_expr is not None and time_expr is not None:
3064            # In DuckDB, DATE + TIME produces TIMESTAMP
3065            return self.sql(exp.Add(this=date_expr, expression=time_expr))
3066
3067        # Component-based form: TIMESTAMP_FROM_PARTS(year, month, day, hour, minute, second, ...)
3068        sec = expression.args.get("sec")
3069        if sec is None:
3070            # This shouldn't happen with valid input, but handle gracefully
3071            return rename_func("MAKE_TIMESTAMP")(self, expression)
3072
3073        milli = expression.args.get("milli")
3074        if milli is not None:
3075            sec += milli.pop() / exp.Literal.number(1000.0)
3076
3077        nano = expression.args.get("nano")
3078        if nano is not None:
3079            sec += nano.pop() / exp.Literal.number(1000000000.0)
3080
3081        if milli or nano:
3082            expression.set("sec", sec)
3083
3084        return rename_func("MAKE_TIMESTAMP")(self, expression)
3085
3086    @unsupported_args("nano")
3087    def timestampltzfromparts_sql(self, expression: exp.TimestampLtzFromParts) -> str:
3088        # Pop nano so rename_func only passes args that MAKE_TIMESTAMP accepts
3089        if nano := expression.args.get("nano"):
3090            nano.pop()
3091
3092        timestamp = rename_func("MAKE_TIMESTAMP")(self, expression)
3093        return f"CAST({timestamp} AS TIMESTAMPTZ)"
3094
3095    @unsupported_args("nano")
3096    def timestamptzfromparts_sql(self, expression: exp.TimestampTzFromParts) -> str:
3097        # Extract zone before popping
3098        zone = expression.args.get("zone")
3099        # Pop zone and nano so rename_func only passes args that MAKE_TIMESTAMP accepts
3100        if zone:
3101            zone = zone.pop()
3102
3103        if nano := expression.args.get("nano"):
3104            nano.pop()
3105
3106        timestamp = rename_func("MAKE_TIMESTAMP")(self, expression)
3107
3108        if zone:
3109            # Use AT TIME ZONE to apply the explicit timezone
3110            return f"{timestamp} AT TIME ZONE {self.sql(zone)}"
3111
3112        return timestamp
3113
3114    def tablesample_sql(
3115        self,
3116        expression: exp.TableSample,
3117        tablesample_keyword: str | None = None,
3118    ) -> str:
3119        if not isinstance(expression.parent, exp.Select):
3120            # This sample clause only applies to a single source, not the entire resulting relation
3121            tablesample_keyword = "TABLESAMPLE"
3122
3123        if expression.args.get("size"):
3124            method = expression.args.get("method")
3125            if method and method.name.upper() != "RESERVOIR":
3126                self.unsupported(
3127                    f"Sampling method {method} is not supported with a discrete sample count, "
3128                    "defaulting to reservoir sampling"
3129                )
3130                expression.set("method", exp.var("RESERVOIR"))
3131
3132        return super().tablesample_sql(expression, tablesample_keyword=tablesample_keyword)
3133
3134    def in_sql(self, expression: exp.In) -> str:
3135        unnest = expression.args.get("unnest")
3136        if unnest:
3137            return self.sql(
3138                exp.replace_placeholders(
3139                    self.IN_UNNEST_TEMPLATE, arr=unnest.expressions[0], value=expression.this
3140                )
3141            )
3142        return super().in_sql(expression)
3143
3144    def join_sql(self, expression: exp.Join) -> str:
3145        if (
3146            not expression.args.get("using")
3147            and not expression.args.get("on")
3148            and not expression.method
3149            and (expression.kind in ("", "INNER", "OUTER"))
3150        ):
3151            # Some dialects support `LEFT/INNER JOIN UNNEST(...)` without an explicit ON clause
3152            # DuckDB doesn't, but we can just add a dummy ON clause that is always true
3153            if isinstance(expression.this, exp.Unnest):
3154                return super().join_sql(expression.on(exp.true()))
3155
3156            expression.set("side", None)
3157            expression.set("kind", None)
3158
3159        return super().join_sql(expression)
3160
3161    def countif_sql(self, expression: exp.CountIf) -> str:
3162        if self.dialect.version >= (1, 2):
3163            this = expression.this
3164            if expression.args.get("zero_on_all_null") and not isinstance(this, exp.Distinct):
3165                # DuckDB >= 1.2's COUNT_IF returns NULL when the condition is NULL on all rows,
3166                # so we wrap the condition in IS TRUE to preserve count-like semantics
3167                expression = exp.CountIf(this=exp.paren(this).is_(exp.true()))
3168            return self.function_fallback_sql(expression)
3169
3170        # https://github.com/tobymao/sqlglot/pull/4749
3171        return count_if_to_sum(self, expression)
3172
3173    def bracket_sql(self, expression: exp.Bracket) -> str:
3174        if self.dialect.version >= (1, 2):
3175            return super().bracket_sql(expression)
3176
3177        # https://duckdb.org/2025/02/05/announcing-duckdb-120.html#breaking-changes
3178        this = expression.this
3179        if isinstance(this, exp.Array):
3180            this.replace(exp.paren(this))
3181
3182        bracket = super().bracket_sql(expression)
3183
3184        if not expression.args.get("returns_list_for_maps"):
3185            if not this.type:
3186                from sqlglot.optimizer.annotate_types import annotate_types
3187
3188                this = annotate_types(this, dialect=self.dialect)
3189
3190            if this.is_type(exp.DType.MAP):
3191                bracket = f"({bracket})[1]"
3192
3193        return bracket
3194
3195    def withingroup_sql(self, expression: exp.WithinGroup) -> str:
3196        func = expression.this
3197
3198        # For ARRAY_AGG, DuckDB requires ORDER BY inside the function, not in WITHIN GROUP
3199        # Transform: ARRAY_AGG(x) WITHIN GROUP (ORDER BY y) -> ARRAY_AGG(x ORDER BY y)
3200        if isinstance(func, exp.ArrayAgg):
3201            if not isinstance(order := expression.expression, exp.Order):
3202                return self.sql(func)
3203
3204            # Save the original column for FILTER clause (before wrapping with Order)
3205            original_this = func.this
3206
3207            # Move ORDER BY inside ARRAY_AGG by wrapping its argument with Order
3208            # ArrayAgg.this should become Order(this=ArrayAgg.this, expressions=order.expressions)
3209            func.set(
3210                "this",
3211                exp.Order(
3212                    this=func.this.copy(),
3213                    expressions=order.expressions,
3214                ),
3215            )
3216
3217            # Generate the ARRAY_AGG function with ORDER BY and add FILTER clause if needed
3218            # Use original_this (not the Order-wrapped version) for the FILTER condition
3219            array_agg_sql = self.function_fallback_sql(func)
3220            return self._add_arrayagg_null_filter(array_agg_sql, func, original_this)
3221
3222        # For other functions (like PERCENTILES), use existing logic
3223        expression_sql = self.sql(expression, "expression")
3224
3225        if isinstance(func, exp.PERCENTILES):
3226            # Make the order key the first arg and slide the fraction to the right
3227            # https://duckdb.org/docs/sql/aggregates#ordered-set-aggregate-functions
3228            order_col = expression.find(exp.Ordered)
3229            if order_col:
3230                func.set("expression", func.this)
3231                func.set("this", order_col.this)
3232
3233        this = self.sql(expression, "this").rstrip(")")
3234
3235        return f"{this}{expression_sql})"
3236
3237    def length_sql(self, expression: exp.Length) -> str:
3238        arg = expression.this
3239
3240        # Dialects like BQ and Snowflake also accept binary values as args, so
3241        # DDB will attempt to infer the type or resort to case/when resolution
3242        if not expression.args.get("binary") or arg.is_string:
3243            return self.func("LENGTH", arg)
3244
3245        if not arg.type:
3246            from sqlglot.optimizer.annotate_types import annotate_types
3247
3248            arg = annotate_types(arg, dialect=self.dialect)
3249
3250        if arg.is_type(*exp.DataType.TEXT_TYPES):
3251            return self.func("LENGTH", arg)
3252
3253        # We need these casts to make duckdb's static type checker happy
3254        blob = exp.cast(arg, exp.DType.VARBINARY)
3255        varchar = exp.cast(arg, exp.DType.VARCHAR)
3256
3257        case = (
3258            exp.case(exp.Anonymous(this="TYPEOF", expressions=[arg]))
3259            .when(exp.Literal.string("BLOB"), exp.ByteLength(this=blob))
3260            .else_(exp.Anonymous(this="LENGTH", expressions=[varchar]))
3261        )
3262        return self.sql(case)
3263
3264    def bitlength_sql(self, expression: exp.BitLength) -> str:
3265        if not _is_binary(arg := expression.this):
3266            return self.func("BIT_LENGTH", arg)
3267
3268        blob = exp.cast(arg, exp.DataType.Type.VARBINARY)
3269        return self.sql(exp.ByteLength(this=blob) * exp.Literal.number(8))
3270
3271    def chr_sql(self, expression: exp.Chr, name: str = "CHR") -> str:
3272        arg = expression.expressions[0]
3273        if arg.is_type(*exp.DataType.REAL_TYPES):
3274            arg = exp.cast(arg, exp.DType.INT)
3275        return self.func("CHR", arg)
3276
3277    def collation_sql(self, expression: exp.Collation) -> str:
3278        self.unsupported("COLLATION function is not supported by DuckDB")
3279        return self.function_fallback_sql(expression)
3280
3281    def collate_sql(self, expression: exp.Collate) -> str:
3282        if not expression.expression.is_string:
3283            return super().collate_sql(expression)
3284
3285        raw = expression.expression.name
3286        if not raw:
3287            return self.sql(expression.this)
3288
3289        parts = []
3290        for part in raw.split("-"):
3291            lower = part.lower()
3292            if lower not in _SNOWFLAKE_COLLATION_DEFAULTS:
3293                if lower in _SNOWFLAKE_COLLATION_UNSUPPORTED:
3294                    self.unsupported(
3295                        f"Snowflake collation specifier '{part}' has no DuckDB equivalent"
3296                    )
3297                parts.append(lower)
3298
3299        if not parts:
3300            return self.sql(expression.this)
3301        return super().collate_sql(
3302            exp.Collate(this=expression.this, expression=exp.var(".".join(parts)))
3303        )
3304
3305    def _validate_regexp_flags(self, flags: exp.Expr | None, supported_flags: str) -> str | None:
3306        """
3307        Validate and filter regexp flags for DuckDB compatibility.
3308
3309        Args:
3310            flags: The flags expression to validate
3311            supported_flags: String of supported flags (e.g., "ims", "cims").
3312                            Only these flags will be returned.
3313
3314        Returns:
3315            Validated/filtered flag string, or None if no valid flags remain
3316        """
3317        if not isinstance(flags, exp.Expr):
3318            return None
3319
3320        if not flags.is_string:
3321            self.unsupported("Non-literal regexp flags are not fully supported in DuckDB")
3322            return None
3323
3324        flag_str = flags.this
3325        unsupported = set(flag_str) - set(supported_flags)
3326
3327        if unsupported:
3328            self.unsupported(
3329                f"Regexp flags {sorted(unsupported)} are not supported in this context"
3330            )
3331
3332        flag_str = "".join(f for f in flag_str if f in supported_flags)
3333        return flag_str if flag_str else None
3334
3335    def regexpcount_sql(self, expression: exp.RegexpCount) -> str:
3336        this = expression.this
3337        pattern = expression.expression
3338        position = expression.args.get("position")
3339        parameters = expression.args.get("parameters")
3340
3341        # Validate flags - only "ims" flags are supported for embedded patterns
3342        validated_flags = self._validate_regexp_flags(parameters, supported_flags="ims")
3343
3344        if position:
3345            this = exp.Substring(this=this, start=position)
3346
3347        # Embed flags in pattern (REGEXP_EXTRACT_ALL doesn't support flags argument)
3348        if validated_flags:
3349            pattern = exp.Concat(expressions=[exp.Literal.string(f"(?{validated_flags})"), pattern])
3350
3351        # Handle empty pattern: Snowflake returns 0, DuckDB would match between every character
3352        result = (
3353            exp.case()
3354            .when(
3355                exp.EQ(this=pattern, expression=exp.Literal.string("")),
3356                exp.Literal.number(0),
3357            )
3358            .else_(
3359                exp.Length(
3360                    this=exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern])
3361                )
3362            )
3363        )
3364
3365        return self.sql(result)
3366
3367    def regexpreplace_sql(self, expression: exp.RegexpReplace) -> str:
3368        subject = expression.this
3369        pattern = expression.expression
3370        replacement = expression.args.get("replacement") or exp.Literal.string("")
3371        position = expression.args.get("position")
3372        occurrence = expression.args.get("occurrence")
3373        modifiers = expression.args.get("modifiers")
3374
3375        validated_flags = self._validate_regexp_flags(modifiers, supported_flags="cimsg") or ""
3376
3377        # Handle occurrence (only literals supported)
3378        if occurrence and not occurrence.is_int:
3379            self.unsupported("REGEXP_REPLACE with non-literal occurrence")
3380        else:
3381            occurrence = occurrence.to_py() if occurrence and occurrence.is_int else 0
3382            if occurrence > 1:
3383                self.unsupported(f"REGEXP_REPLACE occurrence={occurrence} not supported")
3384            # flag duckdb to do either all or none, single_replace check is for duckdb round trip
3385            elif (
3386                occurrence == 0
3387                and "g" not in validated_flags
3388                and not expression.args.get("single_replace")
3389            ):
3390                validated_flags += "g"
3391
3392        # Handle position (only literals supported)
3393        prefix = None
3394        if position and not position.is_int:
3395            self.unsupported("REGEXP_REPLACE with non-literal position")
3396        elif position and position.is_int and position.to_py() > 1:
3397            pos = position.to_py()
3398            prefix = exp.Substring(
3399                this=subject, start=exp.Literal.number(1), length=exp.Literal.number(pos - 1)
3400            )
3401            subject = exp.Substring(this=subject, start=exp.Literal.number(pos))
3402
3403        result: exp.Expr = exp.Anonymous(
3404            this="REGEXP_REPLACE",
3405            expressions=[
3406                subject,
3407                pattern,
3408                replacement,
3409                exp.Literal.string(validated_flags) if validated_flags else None,
3410            ],
3411        )
3412
3413        if prefix:
3414            result = exp.Concat(expressions=[prefix, result])
3415
3416        return self.sql(result)
3417
3418    def regexplike_sql(self, expression: exp.RegexpLike) -> str:
3419        this = expression.this
3420        pattern = expression.expression
3421        flag = expression.args.get("flag")
3422
3423        if expression.args.get("full_match"):
3424            validated_flags = self._validate_regexp_flags(flag, supported_flags="cims")
3425            flag = exp.Literal.string(validated_flags) if validated_flags else None
3426            return self.func("REGEXP_FULL_MATCH", this, pattern, flag)
3427
3428        return self.func("REGEXP_MATCHES", this, pattern, flag)
3429
3430    @unsupported_args("ins_cost", "del_cost", "sub_cost")
3431    def levenshtein_sql(self, expression: exp.Levenshtein) -> str:
3432        this = expression.this
3433        expr = expression.expression
3434        max_dist = expression.args.get("max_dist")
3435
3436        if max_dist is None:
3437            return self.func("LEVENSHTEIN", this, expr)
3438
3439        # Emulate Snowflake semantics: if distance > max_dist, return max_dist
3440        levenshtein = exp.Levenshtein(this=this, expression=expr)
3441        return self.sql(exp.Least(this=levenshtein, expressions=[max_dist]))
3442
3443    def pad_sql(self, expression: exp.Pad) -> str:
3444        """
3445        Handle RPAD/LPAD for VARCHAR and BINARY types.
3446
3447        For VARCHAR: Delegate to parent class
3448        For BINARY: Lower to: input || REPEAT(pad, GREATEST(0, target_len - OCTET_LENGTH(input)))
3449        """
3450        string_arg = expression.this
3451        fill_arg = expression.args.get("fill_pattern") or exp.Literal.string(" ")
3452
3453        if _is_binary(string_arg) or _is_binary(fill_arg):
3454            length_arg = expression.expression
3455            is_left = expression.args.get("is_left")
3456
3457            input_len = exp.ByteLength(this=string_arg)
3458            chars_needed = length_arg - input_len
3459            pad_count = exp.Greatest(
3460                this=exp.Literal.number(0), expressions=[chars_needed], ignore_nulls=True
3461            )
3462            repeat_expr = exp.Repeat(this=fill_arg, times=pad_count)
3463
3464            left, right = string_arg, repeat_expr
3465            if is_left:
3466                left, right = right, left
3467
3468            result = exp.DPipe(this=left, expression=right)
3469            return self.sql(result)
3470
3471        # For VARCHAR: Delegate to parent class (handles PAD_FILL_PATTERN_IS_REQUIRED)
3472        return super().pad_sql(expression)
3473
3474    def minhash_sql(self, expression: exp.Minhash) -> str:
3475        k = expression.this
3476        exprs = expression.expressions
3477
3478        if len(exprs) != 1 or isinstance(exprs[0], exp.Star):
3479            self.unsupported(
3480                "MINHASH with multiple expressions or * requires manual query restructuring"
3481            )
3482            return self.func("MINHASH", k, *exprs)
3483
3484        expr = exprs[0]
3485        result = exp.replace_placeholders(self.MINHASH_TEMPLATE.copy(), expr=expr, k=k)
3486        return f"({self.sql(result)})"
3487
3488    def minhashcombine_sql(self, expression: exp.MinhashCombine) -> str:
3489        expr = expression.this
3490        result = exp.replace_placeholders(self.MINHASH_COMBINE_TEMPLATE.copy(), expr=expr)
3491        return f"({self.sql(result)})"
3492
3493    def approximatesimilarity_sql(self, expression: exp.ApproximateSimilarity) -> str:
3494        expr = expression.this
3495        result = exp.replace_placeholders(self.APPROXIMATE_SIMILARITY_TEMPLATE.copy(), expr=expr)
3496        return f"({self.sql(result)})"
3497
3498    def arrayuniqueagg_sql(self, expression: exp.ArrayUniqueAgg) -> str:
3499        return self.sql(
3500            exp.Filter(
3501                this=exp.func("LIST", exp.Distinct(expressions=[expression.this])),
3502                expression=exp.Where(this=expression.this.copy().is_(exp.null()).not_()),
3503            )
3504        )
3505
3506    def arrayconcatagg_sql(self, expression: exp.ArrayConcatAgg) -> str:
3507        this = expression.this
3508
3509        if isinstance(this, exp.Limit):
3510            self.unsupported("LIMIT in ARRAY_CONCAT_AGG cannot be transpiled to DuckDB")
3511            this = this.this
3512
3513        inner = this.this if isinstance(this, exp.Order) else this
3514
3515        return self.func(
3516            "FLATTEN",
3517            exp.Filter(
3518                this=exp.ArrayAgg(this=this),
3519                expression=exp.Where(this=inner.copy().is_(exp.null()).not_()),
3520            ),
3521        )
3522
3523    def arrayunionagg_sql(self, expression: exp.ArrayUnionAgg) -> str:
3524        self.unsupported("ARRAY_UNION_AGG is not supported in DuckDB")
3525        return self.function_fallback_sql(expression)
3526
3527    def arraydistinct_sql(self, expression: exp.ArrayDistinct) -> str:
3528        arr = expression.this
3529        func = self.func("LIST_DISTINCT", arr)
3530
3531        if expression.args.get("check_null"):
3532            add_null_to_array = exp.func(
3533                "LIST_APPEND", exp.func("LIST_DISTINCT", exp.ArrayCompact(this=arr)), exp.Null()
3534            )
3535            return self.sql(
3536                exp.If(
3537                    this=exp.NEQ(
3538                        this=exp.ArraySize(this=arr), expression=exp.func("LIST_COUNT", arr)
3539                    ),
3540                    true=add_null_to_array,
3541                    false=func,
3542                )
3543            )
3544
3545        return func
3546
3547    def arrayintersect_sql(self, expression: exp.ArrayIntersect) -> str:
3548        if expression.args.get("is_multiset") and len(expression.expressions) == 2:
3549            return self._array_bag_sql(
3550                self.ARRAY_INTERSECTION_CONDITION,
3551                expression.expressions[0],
3552                expression.expressions[1],
3553            )
3554        return self.function_fallback_sql(expression)
3555
3556    def arrayexcept_sql(self, expression: exp.ArrayExcept) -> str:
3557        arr1, arr2 = expression.this, expression.expression
3558        if expression.args.get("is_multiset"):
3559            return self._array_bag_sql(self.ARRAY_EXCEPT_CONDITION, arr1, arr2)
3560        return self.sql(
3561            exp.replace_placeholders(self.ARRAY_EXCEPT_SET_TEMPLATE, arr1=arr1, arr2=arr2)
3562        )
3563
3564    def arrayslice_sql(self, expression: exp.ArraySlice) -> str:
3565        """
3566        Transpiles Snowflake's ARRAY_SLICE (0-indexed, exclusive end) to DuckDB's
3567        ARRAY_SLICE (1-indexed, inclusive end) by wrapping start and end in CASE
3568        expressions that adjust the index at query time:
3569          - start: CASE WHEN start >= 0 THEN start + 1 ELSE start END
3570          - end:   CASE WHEN end < 0 THEN end - 1 ELSE end END
3571        """
3572        start, end = expression.args.get("start"), expression.args.get("end")
3573
3574        if expression.args.get("zero_based"):
3575            if start is not None:
3576                start = (
3577                    exp.case()
3578                    .when(
3579                        exp.GTE(this=start.copy(), expression=exp.Literal.number(0)),
3580                        exp.Add(this=start.copy(), expression=exp.Literal.number(1)),
3581                    )
3582                    .else_(start)
3583                )
3584            if end is not None:
3585                end = (
3586                    exp.case()
3587                    .when(
3588                        exp.LT(this=end.copy(), expression=exp.Literal.number(0)),
3589                        exp.Sub(this=end.copy(), expression=exp.Literal.number(1)),
3590                    )
3591                    .else_(end)
3592                )
3593
3594        return self.func("ARRAY_SLICE", expression.this, start, end, expression.args.get("step"))
3595
3596    def arrayszip_sql(self, expression: exp.ArraysZip) -> str:
3597        args = expression.expressions
3598
3599        if not args:
3600            # Return [{}] - using MAP([], []) since DuckDB can't represent empty structs
3601            return self.sql(exp.array(exp.Map(keys=exp.array(), values=exp.array())))
3602
3603        # Build placeholder values for template
3604        lengths = [exp.Length(this=arg) for arg in args]
3605        max_len = (
3606            lengths[0]
3607            if len(lengths) == 1
3608            else exp.Greatest(this=lengths[0], expressions=lengths[1:])
3609        )
3610
3611        # Empty struct with same schema: {'$1': NULL, '$2': NULL, ...}
3612        empty_struct = exp.func(
3613            "STRUCT",
3614            *[
3615                exp.PropertyEQ(this=exp.Literal.string(f"${i + 1}"), expression=exp.Null())
3616                for i in range(len(args))
3617            ],
3618        )
3619
3620        # Struct for transform: {'$1': COALESCE(arr1, [])[__i + 1], ...}
3621        # COALESCE wrapping handles NULL arrays - prevents invalid NULL[i] syntax
3622        index = exp.column("__i") + 1
3623        transform_struct = exp.func(
3624            "STRUCT",
3625            *[
3626                exp.PropertyEQ(
3627                    this=exp.Literal.string(f"${i + 1}"),
3628                    expression=exp.func("COALESCE", arg, exp.array())[index],
3629                )
3630                for i, arg in enumerate(args)
3631            ],
3632        )
3633
3634        result = exp.replace_placeholders(
3635            self.ARRAYS_ZIP_TEMPLATE.copy(),
3636            null_check=exp.or_(*[arg.is_(exp.Null()) for arg in args]),
3637            all_empty_check=exp.and_(
3638                *[
3639                    exp.EQ(this=exp.Length(this=arg), expression=exp.Literal.number(0))
3640                    for arg in args
3641                ]
3642            ),
3643            empty_struct=empty_struct,
3644            max_len=max_len,
3645            transform_struct=transform_struct,
3646        )
3647        return self.sql(result)
3648
3649    def lower_sql(self, expression: exp.Lower) -> str:
3650        result_sql = self.func("LOWER", _cast_to_varchar(expression.this))
3651        return _gen_with_cast_to_blob(self, expression, result_sql)
3652
3653    def upper_sql(self, expression: exp.Upper) -> str:
3654        result_sql = self.func("UPPER", _cast_to_varchar(expression.this))
3655        return _gen_with_cast_to_blob(self, expression, result_sql)
3656
3657    def reverse_sql(self, expression: exp.Reverse) -> str:
3658        result_sql = self.func("REVERSE", _cast_to_varchar(expression.this))
3659        return _gen_with_cast_to_blob(self, expression, result_sql)
3660
3661    def _left_right_sql(self, expression: exp.Left | exp.Right, func_name: str) -> str:
3662        arg = expression.this
3663        length = expression.expression
3664        is_binary = _is_binary(arg)
3665
3666        if is_binary:
3667            # LEFT/RIGHT(blob, n) becomes UNHEX(LEFT/RIGHT(HEX(blob), n * 2))
3668            # Each byte becomes 2 hex chars, so multiply length by 2
3669            hex_arg = exp.Hex(this=arg)
3670            hex_length = exp.Mul(this=length, expression=exp.Literal.number(2))
3671            result: exp.Expression = exp.Unhex(
3672                this=exp.Anonymous(this=func_name, expressions=[hex_arg, hex_length])
3673            )
3674        else:
3675            result = exp.Anonymous(this=func_name, expressions=[arg, length])
3676
3677        if expression.args.get("negative_length_returns_empty"):
3678            empty: exp.Expression = exp.Literal.string("")
3679            if is_binary:
3680                empty = exp.Unhex(this=empty)
3681            result = exp.case().when(length < exp.Literal.number(0), empty).else_(result)
3682
3683        return self.sql(result)
3684
3685    def left_sql(self, expression: exp.Left) -> str:
3686        return self._left_right_sql(expression, "LEFT")
3687
3688    def right_sql(self, expression: exp.Right) -> str:
3689        return self._left_right_sql(expression, "RIGHT")
3690
3691    def rtrimmedlength_sql(self, expression: exp.RtrimmedLength) -> str:
3692        return self.func("LENGTH", exp.Trim(this=expression.this, position="TRAILING"))
3693
3694    def stuff_sql(self, expression: exp.Stuff) -> str:
3695        base = expression.this
3696        start = expression.args["start"]
3697        length = expression.args["length"]
3698        insertion = expression.expression
3699        is_binary = _is_binary(base)
3700
3701        if is_binary:
3702            # DuckDB's SUBSTRING doesn't accept BLOB; operate on the HEX string instead
3703            # (each byte = 2 hex chars), then UNHEX back to BLOB
3704            base = exp.Hex(this=base)
3705            insertion = exp.Hex(this=insertion)
3706            left = exp.Substring(
3707                this=base.copy(),
3708                start=exp.Literal.number(1),
3709                length=(start.copy() - exp.Literal.number(1)) * exp.Literal.number(2),
3710            )
3711            right = exp.Substring(
3712                this=base.copy(),
3713                start=((start + length) - exp.Literal.number(1)) * exp.Literal.number(2)
3714                + exp.Literal.number(1),
3715            )
3716        else:
3717            left = exp.Substring(
3718                this=base.copy(),
3719                start=exp.Literal.number(1),
3720                length=start.copy() - exp.Literal.number(1),
3721            )
3722            right = exp.Substring(this=base.copy(), start=start + length)
3723        result: exp.Expr = exp.DPipe(
3724            this=exp.DPipe(this=left, expression=insertion), expression=right
3725        )
3726
3727        if is_binary:
3728            result = exp.Unhex(this=result)
3729
3730        return self.sql(result)
3731
3732    def rand_sql(self, expression: exp.Rand) -> str:
3733        seed = expression.this
3734        if seed is not None:
3735            self.unsupported("RANDOM with seed is not supported in DuckDB")
3736
3737        lower = expression.args.get("lower")
3738        upper = expression.args.get("upper")
3739
3740        if lower and upper:
3741            # scale DuckDB's [0,1) to the specified range
3742            range_size = exp.paren(upper - lower)
3743            scaled = exp.Add(this=lower, expression=exp.func("random") * range_size)
3744
3745            # For now we assume that if bounds are set, return type is BIGINT. Snowflake/Teradata
3746            result = exp.cast(scaled, exp.DType.BIGINT)
3747            return self.sql(result)
3748
3749        # Default DuckDB behavior - just return RANDOM() as float
3750        return "RANDOM()"
3751
3752    def bytelength_sql(self, expression: exp.ByteLength) -> str:
3753        arg = expression.this
3754
3755        # Check if it's a text type (handles both literals and annotated expressions)
3756        if arg.is_type(*exp.DataType.TEXT_TYPES):
3757            return self.func("OCTET_LENGTH", exp.Encode(this=arg))
3758
3759        # Default: pass through as-is (conservative for DuckDB, handles binary and unannotated)
3760        return self.func("OCTET_LENGTH", arg)
3761
3762    def base64encode_sql(self, expression: exp.Base64Encode) -> str:
3763        # DuckDB TO_BASE64 requires BLOB input
3764        # Snowflake BASE64_ENCODE accepts both VARCHAR and BINARY - for VARCHAR it implicitly
3765        # encodes UTF-8 bytes. We add ENCODE unless the input is a binary type.
3766        result = expression.this
3767
3768        # Check if input is a string type - ENCODE only accepts VARCHAR
3769        if result.is_type(*exp.DataType.TEXT_TYPES):
3770            result = exp.Encode(this=result)
3771
3772        result = exp.ToBase64(this=result)
3773
3774        max_line_length = expression.args.get("max_line_length")
3775        alphabet = expression.args.get("alphabet")
3776
3777        # Handle custom alphabet by replacing standard chars with custom ones
3778        result = _apply_base64_alphabet_replacements(result, alphabet)
3779
3780        # Handle max_line_length by inserting newlines every N characters
3781        line_length = (
3782            t.cast(int, max_line_length.to_py())
3783            if isinstance(max_line_length, exp.Literal) and max_line_length.is_number
3784            else 0
3785        )
3786        if line_length > 0:
3787            newline = exp.Chr(expressions=[exp.Literal.number(10)])
3788            result = exp.Trim(
3789                this=exp.RegexpReplace(
3790                    this=result,
3791                    expression=exp.Literal.string(f"(.{{{line_length}}})"),
3792                    replacement=exp.Concat(expressions=[exp.Literal.string("\\1"), newline.copy()]),
3793                ),
3794                expression=newline,
3795                position="TRAILING",
3796            )
3797
3798        return self.sql(result)
3799
3800    def hex_sql(self, expression: exp.Hex) -> str:
3801        case = expression.args.get("case")
3802
3803        if not case:
3804            return self.func("HEX", expression.this)
3805
3806        hex_expr = exp.Hex(this=expression.this)
3807        return self.sql(
3808            exp.case()
3809            .when(case.is_(exp.null()), exp.null())
3810            .when(case.copy().eq(0), exp.Lower(this=hex_expr.copy()))
3811            .else_(hex_expr)
3812        )
3813
3814    def replace_sql(self, expression: exp.Replace) -> str:
3815        result_sql = self.func(
3816            "REPLACE",
3817            _cast_to_varchar(expression.this),
3818            _cast_to_varchar(expression.expression),
3819            _cast_to_varchar(expression.args.get("replacement")),
3820        )
3821        return _gen_with_cast_to_blob(self, expression, result_sql)
3822
3823    def _bitwise_op(self, expression: exp.Binary, op: str) -> str:
3824        _prepare_binary_bitwise_args(expression)
3825        result_sql = self.binary(expression, op)
3826        return _gen_with_cast_to_blob(self, expression, result_sql)
3827
3828    def bitwisexor_sql(self, expression: exp.BitwiseXor) -> str:
3829        _prepare_binary_bitwise_args(expression)
3830        result_sql = self.func("XOR", expression.this, expression.expression)
3831        return _gen_with_cast_to_blob(self, expression, result_sql)
3832
3833    def objectinsert_sql(self, expression: exp.ObjectInsert) -> str:
3834        this = expression.this
3835        key = expression.args.get("key")
3836        key_sql = key.name if isinstance(key, exp.Expr) else ""
3837        value_sql = self.sql(expression, "value")
3838
3839        kv_sql = f"{key_sql} := {value_sql}"
3840
3841        # If the input struct is empty e.g. transpiling OBJECT_INSERT(OBJECT_CONSTRUCT(), key, value) from Snowflake
3842        # then we can generate STRUCT_PACK which will build it since STRUCT_INSERT({}, key := value) is not valid DuckDB
3843        if isinstance(this, exp.Struct) and not this.expressions:
3844            return self.func("STRUCT_PACK", kv_sql)
3845
3846        return self.func("STRUCT_INSERT", this, kv_sql)
3847
3848    def mapcat_sql(self, expression: exp.MapCat) -> str:
3849        result = exp.replace_placeholders(
3850            self.MAPCAT_TEMPLATE.copy(),
3851            map1=expression.this,
3852            map2=expression.expression,
3853        )
3854        return self.sql(result)
3855
3856    def mapcontainskey_sql(self, expression: exp.MapContainsKey) -> str:
3857        return self.func(
3858            "ARRAY_CONTAINS", exp.func("MAP_KEYS", expression.args["key"]), expression.this
3859        )
3860
3861    def mapdelete_sql(self, expression: exp.MapDelete) -> str:
3862        map_arg = expression.this
3863        keys_to_delete = expression.expressions
3864
3865        x_dot_key = exp.Dot(this=exp.to_identifier("x"), expression=exp.to_identifier("key"))
3866
3867        lambda_expr = exp.Lambda(
3868            this=exp.In(this=x_dot_key, expressions=keys_to_delete).not_(),
3869            expressions=[exp.to_identifier("x")],
3870        )
3871        result = exp.func(
3872            "MAP_FROM_ENTRIES",
3873            exp.ArrayFilter(this=exp.func("MAP_ENTRIES", map_arg), expression=lambda_expr),
3874        )
3875        return self.sql(result)
3876
3877    def mappick_sql(self, expression: exp.MapPick) -> str:
3878        map_arg = expression.this
3879        keys_to_pick = expression.expressions
3880
3881        x_dot_key = exp.Dot(this=exp.to_identifier("x"), expression=exp.to_identifier("key"))
3882
3883        if len(keys_to_pick) == 1 and keys_to_pick[0].is_type(exp.DType.ARRAY):
3884            lambda_expr = exp.Lambda(
3885                this=exp.func("ARRAY_CONTAINS", keys_to_pick[0], x_dot_key),
3886                expressions=[exp.to_identifier("x")],
3887            )
3888        else:
3889            lambda_expr = exp.Lambda(
3890                this=exp.In(this=x_dot_key, expressions=keys_to_pick),
3891                expressions=[exp.to_identifier("x")],
3892            )
3893
3894        result = exp.func(
3895            "MAP_FROM_ENTRIES",
3896            exp.func("LIST_FILTER", exp.func("MAP_ENTRIES", map_arg), lambda_expr),
3897        )
3898        return self.sql(result)
3899
3900    def mapsize_sql(self, expression: exp.MapSize) -> str:
3901        return self.func("CARDINALITY", expression.this)
3902
3903    @unsupported_args("update_flag")
3904    def mapinsert_sql(self, expression: exp.MapInsert) -> str:
3905        map_arg = expression.this
3906        key = expression.args.get("key")
3907        value = expression.args.get("value")
3908
3909        map_type = map_arg.type
3910
3911        if value is not None:
3912            if map_type and map_type.expressions and len(map_type.expressions) > 1:
3913                # Extract the value type from MAP(key_type, value_type)
3914                value_type = map_type.expressions[1]
3915                # Cast value to match the map's value type to avoid type conflicts
3916                value = exp.cast(value, value_type)
3917            # else: polymorphic MAP case - no type parameters available, use value as-is
3918
3919        # Create a single-entry map for the new key-value pair
3920        new_entry_struct = exp.Struct(expressions=[exp.PropertyEQ(this=key, expression=value)])
3921        new_entry: exp.Expression = exp.ToMap(this=new_entry_struct)
3922
3923        # Use MAP_CONCAT to merge the original map with the new entry
3924        # This automatically handles both insert and update cases
3925        result = exp.func("MAP_CONCAT", map_arg, new_entry)
3926
3927        return self.sql(result)
3928
3929    def startswith_sql(self, expression: exp.StartsWith) -> str:
3930        return self.func(
3931            "STARTS_WITH",
3932            _cast_to_varchar(expression.this),
3933            _cast_to_varchar(expression.expression),
3934        )
3935
3936    def space_sql(self, expression: exp.Space) -> str:
3937        # DuckDB's REPEAT requires BIGINT for the count parameter
3938        return self.sql(
3939            exp.Repeat(
3940                this=exp.Literal.string(" "),
3941                times=exp.cast(expression.this, exp.DType.BIGINT),
3942            )
3943        )
3944
3945    def tablefromrows_sql(self, expression: exp.TableFromRows) -> str:
3946        # For GENERATOR, unwrap TABLE() - just emit the Generator (becomes RANGE)
3947        if isinstance(expression.this, exp.Generator):
3948            # Preserve alias, joins, and other table-level args
3949            table = exp.Table(
3950                this=expression.this,
3951                alias=expression.args.get("alias"),
3952                joins=expression.args.get("joins"),
3953            )
3954            return self.sql(table)
3955
3956        return super().tablefromrows_sql(expression)
3957
3958    def unnest_sql(self, expression: exp.Unnest) -> str:
3959        explode_array = expression.args.get("explode_array")
3960        if explode_array:
3961            # In BigQuery, UNNESTing a nested array leads to explosion of the top-level array & struct
3962            # This is transpiled to DDB by transforming "FROM UNNEST(...)" to "FROM (SELECT UNNEST(..., max_depth => 2))"
3963            expression.expressions.append(
3964                exp.Kwarg(this=exp.var("max_depth"), expression=exp.Literal.number(2))
3965            )
3966
3967            # If BQ's UNNEST is aliased, we transform it from a column alias to a table alias in DDB
3968            alias = expression.args.get("alias")
3969            if isinstance(alias, exp.TableAlias):
3970                expression.set("alias", None)
3971                if alias.columns:
3972                    alias = exp.TableAlias(this=seq_get(alias.columns, 0))
3973
3974            unnest_sql = super().unnest_sql(expression)
3975            select = exp.Select(expressions=[unnest_sql]).subquery(alias)
3976            return self.sql(select)
3977
3978        return super().unnest_sql(expression)
3979
3980    def arrayagg_sql(self, expression: exp.ArrayAgg) -> str:
3981        if isinstance(expression.this, exp.Limit):
3982            self.unsupported("LIMIT inside ARRAY_AGG is not supported in DuckDB")
3983
3984        return super().arrayagg_sql(expression)
3985
3986    def ignorenulls_sql(self, expression: exp.IgnoreNulls) -> str:
3987        this = expression.this
3988
3989        if isinstance(this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
3990            # DuckDB should render IGNORE NULLS only for the general-purpose
3991            # window functions that accept it e.g. FIRST_VALUE(... IGNORE NULLS) OVER (...)
3992            return super().ignorenulls_sql(expression)
3993
3994        # For ARRAY_AGG(expr IGNORE NULLS ...), convert IGNORE NULLS to a
3995        # FILTER(WHERE expr IS NOT NULL) clause by setting nulls_excluded on
3996        # the ArrayAgg.  The existing _add_arrayagg_null_filter method will
3997        # emit the FILTER clause during arrayagg_sql / withingroup_sql.
3998        if isinstance(this, exp.ArrayAgg):
3999            this.set("nulls_excluded", True)
4000            return self.sql(this)
4001
4002        if isinstance(this, exp.First):
4003            this = exp.AnyValue(this=this.this)
4004
4005        if not isinstance(this, (exp.AnyValue, exp.ApproxQuantiles)):
4006            self.unsupported("IGNORE NULLS is not supported for non-window functions.")
4007
4008        return self.sql(this)
4009
4010    def split_sql(self, expression: exp.Split) -> str:
4011        base_func = exp.func("STR_SPLIT", expression.this, expression.expression)
4012
4013        case_expr = exp.case().else_(base_func)
4014        needs_case = False
4015
4016        if expression.args.get("null_returns_null"):
4017            case_expr = case_expr.when(expression.expression.is_(exp.null()), exp.null())
4018            needs_case = True
4019
4020        if expression.args.get("empty_delimiter_returns_whole"):
4021            # When delimiter is empty string, return input string as single array element
4022            array_with_input = exp.array(expression.this)
4023            case_expr = case_expr.when(
4024                expression.expression.eq(exp.Literal.string("")), array_with_input
4025            )
4026            needs_case = True
4027
4028        return self.sql(case_expr if needs_case else base_func)
4029
4030    def splitpart_sql(self, expression: exp.SplitPart) -> str:
4031        string_arg = expression.this
4032        delimiter_arg = expression.args.get("delimiter")
4033        part_index_arg = expression.args.get("part_index")
4034
4035        if delimiter_arg and part_index_arg:
4036            # Handle Snowflake's "index 0 and 1 both return first element" behavior
4037            if expression.args.get("part_index_zero_as_one"):
4038                # Convert 0 to 1 for compatibility
4039
4040                part_index_arg = exp.Paren(
4041                    this=exp.case()
4042                    .when(part_index_arg.eq(exp.Literal.number("0")), exp.Literal.number("1"))
4043                    .else_(part_index_arg)
4044                )
4045
4046            # Use Anonymous to avoid recursion
4047            base_func_expr: exp.Expr = exp.Anonymous(
4048                this="SPLIT_PART", expressions=[string_arg, delimiter_arg, part_index_arg]
4049            )
4050            needs_case_transform = False
4051            case_expr = exp.case().else_(base_func_expr)
4052
4053            if expression.args.get("empty_delimiter_returns_whole"):
4054                # When delimiter is empty string:
4055                # - Return whole string if part_index is 1 or -1
4056                # - Return empty string otherwise
4057                empty_case = exp.Paren(
4058                    this=exp.case()
4059                    .when(
4060                        exp.or_(
4061                            part_index_arg.eq(exp.Literal.number("1")),
4062                            part_index_arg.eq(exp.Literal.number("-1")),
4063                        ),
4064                        string_arg,
4065                    )
4066                    .else_(exp.Literal.string(""))
4067                )
4068
4069                case_expr = case_expr.when(delimiter_arg.eq(exp.Literal.string("")), empty_case)
4070                needs_case_transform = True
4071
4072            """
4073            Output looks something like this:
4074
4075            CASE
4076            WHEN delimiter is '' THEN
4077                (
4078                    CASE
4079                    WHEN adjusted_part_index = 1 OR adjusted_part_index = -1 THEN input
4080                    ELSE '' END
4081                )
4082            ELSE SPLIT_PART(input, delimiter, adjusted_part_index)
4083            END
4084
4085            """
4086            return self.sql(case_expr if needs_case_transform else base_func_expr)
4087
4088        return self.function_fallback_sql(expression)
4089
4090    def respectnulls_sql(self, expression: exp.RespectNulls) -> str:
4091        if isinstance(expression.this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
4092            # DuckDB should render RESPECT NULLS only for the general-purpose
4093            # window functions that accept it e.g. FIRST_VALUE(... RESPECT NULLS) OVER (...)
4094            return super().respectnulls_sql(expression)
4095
4096        self.unsupported("RESPECT NULLS is not supported for non-window functions.")
4097        return self.sql(expression, "this")
4098
4099    def arraytostring_sql(self, expression: exp.ArrayToString) -> str:
4100        null = expression.args.get("null")
4101
4102        if expression.args.get("null_is_empty"):
4103            x = exp.to_identifier("x")
4104            list_transform = exp.Transform(
4105                this=expression.this.copy(),
4106                expression=exp.Lambda(
4107                    this=exp.Coalesce(
4108                        this=exp.cast(x, "TEXT"), expressions=[exp.Literal.string("")]
4109                    ),
4110                    expressions=[x],
4111                ),
4112            )
4113            array_to_string = exp.ArrayToString(
4114                this=list_transform, expression=expression.expression
4115            )
4116            if expression.args.get("null_delim_is_null"):
4117                return self.sql(
4118                    exp.case()
4119                    .when(expression.expression.copy().is_(exp.null()), exp.null())
4120                    .else_(array_to_string)
4121                )
4122            return self.sql(array_to_string)
4123
4124        if null:
4125            x = exp.to_identifier("x")
4126            return self.sql(
4127                exp.ArrayToString(
4128                    this=exp.Transform(
4129                        this=expression.this,
4130                        expression=exp.Lambda(
4131                            this=exp.Coalesce(this=x, expressions=[null]),
4132                            expressions=[x],
4133                        ),
4134                    ),
4135                    expression=expression.expression,
4136                )
4137            )
4138
4139        return self.func("ARRAY_TO_STRING", expression.this, expression.expression)
4140
4141    def concatws_sql(self, expression: exp.ConcatWs) -> str:
4142        # DuckDB-specific: handle binary types using DPipe (||) operator
4143        separator = seq_get(expression.expressions, 0)
4144        args = expression.expressions[1:]
4145
4146        if any(_is_binary(arg) for arg in [separator, *args]):
4147            result = args[0]
4148            for arg in args[1:]:
4149                result = exp.DPipe(
4150                    this=exp.DPipe(this=result, expression=separator), expression=arg
4151                )
4152            return self.sql(result)
4153
4154        return super().concatws_sql(expression)
4155
4156    def _regexp_extract_sql(self, expression: exp.RegexpExtract | exp.RegexpExtractAll) -> str:
4157        this = expression.this
4158        group = expression.args.get("group")
4159        params = expression.args.get("parameters")
4160        position = expression.args.get("position")
4161        occurrence = expression.args.get("occurrence")
4162        null_if_pos_overflow = expression.args.get("null_if_pos_overflow")
4163
4164        # Handle Snowflake's 'e' flag: it enables capture group extraction
4165        # In DuckDB, this is controlled by the group parameter directly
4166        if params and params.is_string and "e" in params.name:
4167            params = exp.Literal.string(params.name.replace("e", ""))
4168
4169        validated_flags = self._validate_regexp_flags(params, supported_flags="cims")
4170
4171        # Strip default group when no following params (DuckDB default is same as group=0)
4172        if (
4173            not validated_flags
4174            and group
4175            and group.name == str(self.dialect.REGEXP_EXTRACT_DEFAULT_GROUP)
4176        ):
4177            group = None
4178
4179        flags_expr = exp.Literal.string(validated_flags) if validated_flags else None
4180
4181        # use substring to handle position argument
4182        if position and (not position.is_int or position.to_py() > 1):
4183            this = exp.Substring(this=this, start=position)
4184
4185            if null_if_pos_overflow:
4186                this = exp.Nullif(this=this, expression=exp.Literal.string(""))
4187
4188        is_extract_all = isinstance(expression, exp.RegexpExtractAll)
4189        non_single_occurrence = occurrence and (not occurrence.is_int or occurrence.to_py() > 1)
4190
4191        if is_extract_all or non_single_occurrence:
4192            name = "REGEXP_EXTRACT_ALL"
4193        else:
4194            name = "REGEXP_EXTRACT"
4195
4196        result: exp.Expr = exp.Anonymous(
4197            this=name, expressions=[this, expression.expression, group, flags_expr]
4198        )
4199
4200        # Array slicing for REGEXP_EXTRACT_ALL with occurrence
4201        if is_extract_all and non_single_occurrence:
4202            result = exp.Bracket(this=result, expressions=[exp.Slice(this=occurrence)])
4203        # ARRAY_EXTRACT for REGEXP_EXTRACT with occurrence > 1
4204        elif non_single_occurrence:
4205            result = exp.Anonymous(this="ARRAY_EXTRACT", expressions=[result, occurrence])
4206
4207        return self.sql(result)
4208
4209    def regexpextract_sql(self, expression: exp.RegexpExtract) -> str:
4210        return self._regexp_extract_sql(expression)
4211
4212    def regexpextractall_sql(self, expression: exp.RegexpExtractAll) -> str:
4213        return self._regexp_extract_sql(expression)
4214
4215    def regexpinstr_sql(self, expression: exp.RegexpInstr) -> str:
4216        this = expression.this
4217        pattern = expression.expression
4218        position = expression.args.get("position")
4219        orig_occ = expression.args.get("occurrence")
4220        occurrence = orig_occ or exp.Literal.number(1)
4221        option = expression.args.get("option")
4222        parameters = expression.args.get("parameters")
4223
4224        validated_flags = self._validate_regexp_flags(parameters, supported_flags="ims")
4225        if validated_flags:
4226            pattern = exp.Concat(expressions=[exp.Literal.string(f"(?{validated_flags})"), pattern])
4227
4228        # Handle starting position offset
4229        pos_offset: exp.Expr = exp.Literal.number(0)
4230        if position and (not position.is_int or position.to_py() > 1):
4231            this = exp.Substring(this=this, start=position)
4232            pos_offset = position - exp.Literal.number(1)
4233
4234        # Helper: LIST_SUM(LIST_TRANSFORM(list[1:end], x -> LENGTH(x)))
4235        def sum_lengths(func_name: str, end: exp.Expr) -> exp.Expr:
4236            lst = exp.Bracket(
4237                this=exp.Anonymous(this=func_name, expressions=[this, pattern]),
4238                expressions=[exp.Slice(this=exp.Literal.number(1), expression=end)],
4239                offset=1,
4240            )
4241            transform = exp.Anonymous(
4242                this="LIST_TRANSFORM",
4243                expressions=[
4244                    lst,
4245                    exp.Lambda(
4246                        this=exp.Length(this=exp.to_identifier("x")),
4247                        expressions=[exp.to_identifier("x")],
4248                    ),
4249                ],
4250            )
4251            return exp.Coalesce(
4252                this=exp.Anonymous(this="LIST_SUM", expressions=[transform]),
4253                expressions=[exp.Literal.number(0)],
4254            )
4255
4256        # Position = 1 + sum(split_lengths[1:occ]) + sum(match_lengths[1:occ-1]) + offset
4257        base_pos: exp.Expr = (
4258            exp.Literal.number(1)
4259            + sum_lengths("STRING_SPLIT_REGEX", occurrence)
4260            + sum_lengths("REGEXP_EXTRACT_ALL", occurrence - exp.Literal.number(1))
4261            + pos_offset
4262        )
4263
4264        # option=1: add match length for end position
4265        if option and option.is_int and option.to_py() == 1:
4266            match_at_occ = exp.Bracket(
4267                this=exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern]),
4268                expressions=[occurrence],
4269                offset=1,
4270            )
4271            base_pos = base_pos + exp.Coalesce(
4272                this=exp.Length(this=match_at_occ), expressions=[exp.Literal.number(0)]
4273            )
4274
4275        # NULL checks for all provided arguments
4276        # .copy() is used strictly because .is_() alters the node's parent pointer, mutating the parsed AST
4277        null_args = [
4278            expression.this,
4279            expression.expression,
4280            position,
4281            orig_occ,
4282            option,
4283            parameters,
4284        ]
4285        null_checks = [arg.copy().is_(exp.Null()) for arg in null_args if arg]
4286
4287        matches = exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern])
4288
4289        return self.sql(
4290            exp.case()
4291            .when(exp.or_(*null_checks), exp.Null())
4292            .when(pattern.copy().eq(exp.Literal.string("")), exp.Literal.number(0))
4293            .when(exp.Length(this=matches) < occurrence, exp.Literal.number(0))
4294            .else_(base_pos)
4295        )
4296
4297    @unsupported_args("culture")
4298    def numbertostr_sql(self, expression: exp.NumberToStr) -> str:
4299        fmt = expression.args.get("format")
4300        if fmt and fmt.is_int:
4301            return self.func("FORMAT", f"'{{:,.{fmt.name}f}}'", expression.this)
4302
4303        self.unsupported("Only integer formats are supported by NumberToStr")
4304        return self.function_fallback_sql(expression)
4305
4306    def autoincrementcolumnconstraint_sql(self, _) -> str:
4307        self.unsupported("The AUTOINCREMENT column constraint is not supported by DuckDB")
4308        return ""
4309
4310    def aliases_sql(self, expression: exp.Aliases) -> str:
4311        this = expression.this
4312        if isinstance(this, exp.Posexplode):
4313            return self.posexplode_sql(this)
4314
4315        return super().aliases_sql(expression)
4316
4317    def posexplode_sql(self, expression: exp.Posexplode) -> str:
4318        this = expression.this
4319        parent = expression.parent
4320
4321        # The default Spark aliases are "pos" and "col", unless specified otherwise
4322        pos, col = exp.to_identifier("pos"), exp.to_identifier("col")
4323
4324        if isinstance(parent, exp.Aliases):
4325            # Column case: SELECT POSEXPLODE(col) [AS (a, b)]
4326            pos, col = parent.expressions
4327        elif isinstance(parent, exp.Table):
4328            # Table case: SELECT * FROM POSEXPLODE(col) [AS (a, b)]
4329            alias = parent.args.get("alias")
4330            if alias:
4331                pos, col = alias.columns or [pos, col]
4332                alias.pop()
4333
4334        # Translate POSEXPLODE to UNNEST + GENERATE_SUBSCRIPTS
4335        # Note: In Spark pos is 0-indexed, but in DuckDB it's 1-indexed, so we subtract 1 from GENERATE_SUBSCRIPTS
4336        unnest_sql = self.sql(exp.Unnest(expressions=[this], alias=col))
4337        gen_subscripts = self.sql(
4338            exp.Alias(
4339                this=exp.Anonymous(
4340                    this="GENERATE_SUBSCRIPTS", expressions=[this, exp.Literal.number(1)]
4341                )
4342                - exp.Literal.number(1),
4343                alias=pos,
4344            )
4345        )
4346
4347        posexplode_sql = self.format_args(gen_subscripts, unnest_sql)
4348
4349        if isinstance(parent, exp.From) or (parent and isinstance(parent.parent, exp.From)):
4350            # SELECT * FROM POSEXPLODE(col) -> SELECT * FROM (SELECT GENERATE_SUBSCRIPTS(...), UNNEST(...))
4351            return self.sql(exp.Subquery(this=exp.Select(expressions=[posexplode_sql])))
4352
4353        return posexplode_sql
4354
4355    def addmonths_sql(self, expression: exp.AddMonths) -> str:
4356        """
4357        Handles three key issues:
4358        1. Float/decimal months: e.g., Snowflake rounds, whereas DuckDB INTERVAL requires integers
4359        2. End-of-month preservation: If input is last day of month, result is last day of result month
4360        3. Type preservation: Maintains DATE/TIMESTAMPTZ types (DuckDB defaults to TIMESTAMP)
4361        """
4362        from sqlglot.optimizer.annotate_types import annotate_types
4363
4364        this = expression.this
4365        if not this.type:
4366            this = annotate_types(this, dialect=self.dialect)
4367
4368        if this.is_type(*exp.DataType.TEXT_TYPES):
4369            this = exp.Cast(this=this, to=exp.DataType(this=exp.DType.TIMESTAMP))
4370
4371        # Detect float/decimal months to apply rounding (Snowflake behavior)
4372        # DuckDB INTERVAL syntax doesn't support non-integer expressions, so use TO_MONTHS
4373        months_expr = expression.expression
4374        if not months_expr.type:
4375            months_expr = annotate_types(months_expr, dialect=self.dialect)
4376
4377        # Build interval or to_months expression based on type
4378        # Float/decimal case: Round and use TO_MONTHS(CAST(ROUND(value) AS INT))
4379        interval_or_to_months = (
4380            exp.func("TO_MONTHS", exp.cast(exp.func("ROUND", months_expr), "INT"))
4381            if months_expr.is_type(
4382                exp.DType.FLOAT,
4383                exp.DType.DOUBLE,
4384                exp.DType.DECIMAL,
4385            )
4386            # Integer case: standard INTERVAL N MONTH syntax
4387            else exp.Interval(this=months_expr, unit=exp.var("MONTH"))
4388        )
4389
4390        date_add_expr = exp.Add(this=this, expression=interval_or_to_months)
4391
4392        # Apply end-of-month preservation if Snowflake flag is set
4393        # CASE WHEN LAST_DAY(date) = date THEN LAST_DAY(result) ELSE result END
4394        preserve_eom = expression.args.get("preserve_end_of_month")
4395        result_expr = (
4396            exp.case()
4397            .when(
4398                exp.EQ(this=exp.func("LAST_DAY", this), expression=this),
4399                exp.func("LAST_DAY", date_add_expr),
4400            )
4401            .else_(date_add_expr)
4402            if preserve_eom
4403            else date_add_expr
4404        )
4405
4406        # DuckDB's DATE_ADD function returns TIMESTAMP/DATETIME by default, even when the input is DATE
4407        # To match for example Snowflake's ADD_MONTHS behavior (which preserves the input type)
4408        # We need to cast the result back to the original type when the input is DATE or TIMESTAMPTZ
4409        # Example: ADD_MONTHS('2023-01-31'::date, 1) should return DATE, not TIMESTAMP
4410        if this.is_type(exp.DType.DATE, exp.DType.TIMESTAMPTZ):
4411            return self.sql(exp.Cast(this=result_expr, to=this.type))
4412        return self.sql(result_expr)
4413
4414    def format_sql(self, expression: exp.Format) -> str:
4415        if expression.name.lower() == "%s" and len(expression.expressions) == 1:
4416            return self.func("FORMAT", "'{}'", expression.expressions[0])
4417
4418        return self.function_fallback_sql(expression)
4419
4420    def hexstring_sql(
4421        self, expression: exp.HexString, binary_function_repr: str | None = None
4422    ) -> str:
4423        # UNHEX('FF') correctly produces blob \xFF in DuckDB
4424        return super().hexstring_sql(expression, binary_function_repr="UNHEX")
4425
4426    def datetrunc_sql(self, expression: exp.DateTrunc) -> str:
4427        unit = expression.args.get("unit")
4428        date = expression.this
4429
4430        week_start = _week_trunc_start_dow(unit)
4431        unit = unit_to_str(expression)
4432
4433        if week_start:
4434            result = self.sql(
4435                _build_week_trunc_expression(date, week_start, preserve_start_day=True)
4436            )
4437        else:
4438            result = self.func("DATE_TRUNC", unit, date)
4439
4440        if (
4441            expression.args.get("input_type_preserved")
4442            and date.is_type(*exp.DataType.TEMPORAL_TYPES)
4443            and not (is_date_unit(unit) and date.is_type(exp.DType.DATE))
4444        ):
4445            return self.sql(exp.Cast(this=result, to=date.type))
4446
4447        return result
4448
4449    def datetimetrunc_sql(self, expression: exp.DatetimeTrunc) -> str:
4450        this = exp.cast(expression.this, exp.DType.DATETIME)
4451        week_start = _week_trunc_start_dow(expression.args.get("unit"))
4452        if week_start:
4453            return self.sql(
4454                _build_week_trunc_expression(
4455                    this, week_start, preserve_start_day=True, cast_to_date=False
4456                )
4457            )
4458
4459        return self.func("DATE_TRUNC", unit_to_str(expression), this)
4460
4461    def timestamptrunc_sql(self, expression: exp.TimestampTrunc) -> str:
4462        zone = expression.args.get("zone")
4463        timestamp = expression.this
4464        week_start = _week_trunc_start_dow(expression.args.get("unit"))
4465
4466        # The week start emulation below is exact, so avoid weekstart_unit_to_str's degrade warning
4467        unit = unit_to_str(expression) if week_start else weekstart_unit_to_str(self, expression)
4468        date_unit = is_date_unit(unit) or bool(week_start)
4469
4470        def _trunc_expr(this: exp.Expr) -> exp.Expr:
4471            if week_start:
4472                return _build_week_trunc_expression(
4473                    this, week_start, preserve_start_day=True, cast_to_date=False
4474                )
4475            return exp.func("DATE_TRUNC", unit, this)
4476
4477        if date_unit and zone:
4478            # BigQuery's TIMESTAMP_TRUNC with timezone truncates in the target timezone and returns as UTC.
4479            # Double AT TIME ZONE needed for BigQuery compatibility:
4480            # 1. First AT TIME ZONE: ensures truncation happens in the target timezone
4481            # 2. Second AT TIME ZONE: converts the DATE result back to TIMESTAMPTZ (preserving time component)
4482            timestamp = exp.AtTimeZone(this=timestamp, zone=zone)
4483            trunced = _trunc_expr(timestamp)
4484            if isinstance(trunced, exp.DateAdd):
4485                # Parenthesize so the trailing AT TIME ZONE binds to the whole shifted expression
4486                trunced = exp.Paren(this=trunced)
4487            return self.sql(exp.AtTimeZone(this=trunced, zone=zone))
4488
4489        result = self.sql(_trunc_expr(timestamp))
4490        if expression.args.get("input_type_preserved"):
4491            if timestamp.type and timestamp.is_type(exp.DType.TIME, exp.DType.TIMETZ):
4492                dummy_date = exp.Cast(
4493                    this=exp.Literal.string("1970-01-01"),
4494                    to=exp.DataType(this=exp.DType.DATE),
4495                )
4496                date_time = exp.Add(this=dummy_date, expression=timestamp)
4497                result = self.func("DATE_TRUNC", unit, date_time)
4498                return self.sql(exp.Cast(this=result, to=timestamp.type))
4499
4500            if timestamp.is_type(*exp.DataType.TEMPORAL_TYPES) and not (
4501                date_unit and timestamp.is_type(exp.DType.DATE)
4502            ):
4503                return self.sql(exp.Cast(this=result, to=timestamp.type))
4504
4505        return result
4506
4507    def trim_sql(self, expression: exp.Trim) -> str:
4508        expression.this.replace(_cast_to_varchar(expression.this))
4509        if expression.expression:
4510            expression.expression.replace(_cast_to_varchar(expression.expression))
4511
4512        result_sql = super().trim_sql(expression)
4513        return _gen_with_cast_to_blob(self, expression, result_sql)
4514
4515    def round_sql(self, expression: exp.Round) -> str:
4516        this = expression.this
4517        decimals = expression.args.get("decimals")
4518        truncate = expression.args.get("truncate")
4519
4520        # DuckDB requires the scale (decimals) argument to be an INT
4521        # Some dialects (e.g., Snowflake) allow non-integer scales and cast to an integer internally
4522        if decimals is not None and expression.args.get("casts_non_integer_decimals"):
4523            if not (decimals.is_int or decimals.is_type(*exp.DataType.INTEGER_TYPES)):
4524                decimals = exp.cast(decimals, exp.DType.INT)
4525
4526        func = "ROUND"
4527        if truncate:
4528            # BigQuery uses ROUND_HALF_EVEN; Snowflake uses HALF_TO_EVEN
4529            if truncate.this in ("ROUND_HALF_EVEN", "HALF_TO_EVEN"):
4530                func = "ROUND_EVEN"
4531                truncate = None
4532            # BigQuery uses ROUND_HALF_AWAY_FROM_ZERO; Snowflake uses HALF_AWAY_FROM_ZERO
4533            elif truncate.this in ("ROUND_HALF_AWAY_FROM_ZERO", "HALF_AWAY_FROM_ZERO"):
4534                truncate = None
4535
4536        return self.func(func, this, decimals, truncate)
4537
4538    def trycast_sql(self, expression: exp.TryCast) -> str:
4539        to = expression.to
4540        to_type = to.this
4541        src = expression.this
4542
4543        if (
4544            expression.args.get("null_on_text_overflow")
4545            and to_type in exp.DataType.TEXT_TYPES
4546            and to.expressions
4547        ):
4548            return self.sql(
4549                exp.case()
4550                .when(
4551                    exp.LTE(this=exp.func("LENGTH", src), expression=to.expressions[0].this),
4552                    exp.cast(src, "TEXT"),
4553                )
4554                .else_(exp.Null())
4555            )
4556        elif to_type == exp.DType.DATE and expression.args.get("probe_date_format"):
4557            slash_strptime = exp.cast(
4558                exp.func("TRY_STRPTIME", src, exp.Literal.string(self._TRYCAST_DATE_SLASH_FMT)),
4559                "DATE",
4560            )
4561            mon_strptime = exp.cast(
4562                exp.func("TRY_STRPTIME", src, exp.Literal.string(self._TRYCAST_DATE_MON_FMT)),
4563                "DATE",
4564            )
4565            return self.sql(
4566                exp.case()
4567                .when(exp.func("CONTAINS", src, exp.Literal.string("/")), slash_strptime)
4568                .when(
4569                    exp.RegexpLike(this=src, expression=exp.Literal.string("[A-Za-z]")),
4570                    mon_strptime,
4571                )
4572                .else_(exp.TryCast(this=src, to=to))
4573            )
4574        elif (
4575            isinstance(to_type, exp.Interval)
4576            and (unit := to_type.unit)
4577            and expression.args.get("requires_string")
4578        ):
4579            interval_type = exp.DataType.build("INTERVAL")
4580            if isinstance(unit, exp.IntervalSpan):
4581                self.unsupported(
4582                    "TRY_CAST to INTERVAL with span (e.g. HOUR TO MINUTE) is not supported in DuckDB"
4583                )
4584                return self.sql(exp.TryCast(this=src, to=interval_type))
4585            return self.sql(
4586                exp.TryCast(
4587                    this=exp.DPipe(this=src, expression=exp.Literal.string(f" {unit.name}")),
4588                    to=interval_type,
4589                )
4590            )
4591
4592        return super().trycast_sql(expression)
4593
4594    def strtok_sql(self, expression: exp.Strtok) -> str:
4595        string_arg = expression.this
4596        delimiter_arg = expression.args.get("delimiter")
4597        part_index_arg = expression.args.get("part_index")
4598
4599        if delimiter_arg and part_index_arg:
4600            # Escape regex chars and build character class at runtime using REGEXP_REPLACE
4601            escaped_delimiter = exp.Anonymous(
4602                this="REGEXP_REPLACE",
4603                expressions=[
4604                    delimiter_arg,
4605                    exp.Literal.string(
4606                        r"([\[\]^.\-*+?(){}|$\\])"
4607                    ),  # Escape problematic regex chars
4608                    exp.Literal.string(
4609                        r"\\\1"
4610                    ),  # Replace with escaped version using $1 backreference
4611                    exp.Literal.string("g"),  # Global flag
4612                ],
4613            )
4614            # CASE WHEN delimiter = '' THEN '' ELSE CONCAT('[', escaped_delimiter, ']') END
4615            regex_pattern = (
4616                exp.case()
4617                .when(delimiter_arg.eq(exp.Literal.string("")), exp.Literal.string(""))
4618                .else_(
4619                    exp.func(
4620                        "CONCAT",
4621                        exp.Literal.string("["),
4622                        escaped_delimiter,
4623                        exp.Literal.string("]"),
4624                    )
4625                )
4626            )
4627
4628            # STRTOK skips empty strings, so we need to filter them out
4629            # LIST_FILTER(REGEXP_SPLIT_TO_ARRAY(string, pattern), x -> x != '')[index]
4630            split_array = exp.func("REGEXP_SPLIT_TO_ARRAY", string_arg, regex_pattern)
4631            x = exp.to_identifier("x")
4632            is_empty = x.eq(exp.Literal.string(""))
4633            filtered_array = exp.func(
4634                "LIST_FILTER",
4635                split_array,
4636                exp.Lambda(this=exp.not_(is_empty.copy()), expressions=[x.copy()]),
4637            )
4638            base_func = exp.Bracket(
4639                this=filtered_array,
4640                expressions=[part_index_arg],
4641                offset=1,
4642            )
4643
4644            # Use template with the built regex pattern
4645            result = exp.replace_placeholders(
4646                self.STRTOK_TEMPLATE.copy(),
4647                string=string_arg,
4648                delimiter=delimiter_arg,
4649                part_index=part_index_arg,
4650                base_func=base_func,
4651            )
4652
4653            return self.sql(result)
4654
4655        return self.function_fallback_sql(expression)
4656
4657    def strtoktoarray_sql(self, expression: exp.StrtokToArray) -> str:
4658        string_arg = expression.this
4659        delimiter_arg = expression.args.get("expression") or exp.Literal.string(" ")
4660
4661        escaped = exp.RegexpReplace(
4662            this=delimiter_arg.copy(),
4663            expression=exp.Literal.string(r"([\[\]^.\-*+?(){}|$\\])"),
4664            replacement=exp.Literal.string(r"\\\1"),
4665            modifiers=exp.Literal.string("g"),
4666        )
4667        return self.sql(
4668            exp.replace_placeholders(
4669                self.STRTOK_TO_ARRAY_TEMPLATE.copy(),
4670                string=string_arg,
4671                delimiter=delimiter_arg,
4672                escaped=escaped,
4673            )
4674        )
4675
4676    def approxquantile_sql(self, expression: exp.ApproxQuantile) -> str:
4677        result = self.func("APPROX_QUANTILE", expression.this, expression.args.get("quantile"))
4678
4679        # DuckDB returns integers for APPROX_QUANTILE, cast to DOUBLE if the expected type is a real type
4680        if expression.is_type(*exp.DataType.REAL_TYPES):
4681            result = f"CAST({result} AS DOUBLE)"
4682
4683        return result
4684
4685    def approxquantiles_sql(self, expression: exp.ApproxQuantiles) -> str:
4686        """
4687        BigQuery's APPROX_QUANTILES(expr, n) returns an array of n+1 approximate quantile values
4688        dividing the input distribution into n equal-sized buckets.
4689
4690        Both BigQuery and DuckDB use approximate algorithms for quantile estimation, but BigQuery
4691        does not document the specific algorithm used so results may differ. DuckDB does not
4692        support RESPECT NULLS.
4693        """
4694        this = expression.this
4695        if isinstance(this, exp.Distinct):
4696            # APPROX_QUANTILES requires 2 args and DISTINCT node grabs both
4697            if len(this.expressions) < 2:
4698                self.unsupported("APPROX_QUANTILES requires a bucket count argument")
4699                return self.function_fallback_sql(expression)
4700            num_quantiles_expr = this.expressions[1].pop()
4701        else:
4702            num_quantiles_expr = expression.expression
4703
4704        if not isinstance(num_quantiles_expr, exp.Literal) or not num_quantiles_expr.is_int:
4705            self.unsupported("APPROX_QUANTILES bucket count must be a positive integer")
4706            return self.function_fallback_sql(expression)
4707
4708        num_quantiles = t.cast(int, num_quantiles_expr.to_py())
4709        if num_quantiles <= 0:
4710            self.unsupported("APPROX_QUANTILES bucket count must be a positive integer")
4711            return self.function_fallback_sql(expression)
4712
4713        quantiles = [
4714            exp.Literal.number(Decimal(i) / Decimal(num_quantiles))
4715            for i in range(num_quantiles + 1)
4716        ]
4717
4718        return self.sql(exp.ApproxQuantile(this=this, quantile=exp.Array(expressions=quantiles)))
4719
4720    def jsonextractscalar_sql(self, expression: exp.JSONExtractScalar) -> str:
4721        if expression.args.get("scalar_only"):
4722            expression = exp.JSONExtractScalar(
4723                this=rename_func("JSON_VALUE")(self, expression), expression="'$'"
4724            )
4725        return _arrow_json_extract_sql(self, expression)
4726
4727    def bitwisenot_sql(self, expression: exp.BitwiseNot) -> str:
4728        this = expression.this
4729
4730        if _is_binary(this):
4731            expression.type = exp.DType.BINARY.into_expr()
4732
4733        arg = _cast_to_bit(this)
4734
4735        if isinstance(this, exp.Neg):
4736            arg = exp.Paren(this=arg)
4737
4738        expression.set("this", arg)
4739
4740        result_sql = f"~{self.sql(expression, 'this')}"
4741
4742        return _gen_with_cast_to_blob(self, expression, result_sql)
4743
4744    def window_sql(self, expression: exp.Window) -> str:
4745        this = expression.this
4746        if isinstance(this, exp.Corr) or (
4747            isinstance(this, exp.Filter) and isinstance(this.this, exp.Corr)
4748        ):
4749            return self._corr_sql(expression)
4750
4751        return super().window_sql(expression)
4752
4753    def filter_sql(self, expression: exp.Filter) -> str:
4754        if isinstance(expression.this, exp.Corr):
4755            return self._corr_sql(expression)
4756
4757        return super().filter_sql(expression)
4758
4759    def _corr_sql(
4760        self,
4761        expression: exp.Filter | exp.Window | exp.Corr,
4762    ) -> str:
4763        if isinstance(expression, exp.Corr) and not expression.args.get("null_on_zero_variance"):
4764            return self.func("CORR", expression.this, expression.expression)
4765
4766        corr_expr = _maybe_corr_null_to_false(expression)
4767        if corr_expr is None:
4768            if isinstance(expression, exp.Window):
4769                return super().window_sql(expression)
4770            if isinstance(expression, exp.Filter):
4771                return super().filter_sql(expression)
4772            corr_expr = expression  # make mypy happy
4773
4774        return self.sql(exp.case().when(exp.IsNan(this=corr_expr), exp.null()).else_(corr_expr))
4775
4776    def uuid_sql(self, expression: exp.Uuid) -> str:
4777        namespace = expression.this
4778        name = expression.args.get("name")
4779
4780        # UUID v5 (namespace + name) - Emulate using SHA1
4781        if namespace and name:
4782            result = exp.replace_placeholders(
4783                self.UUID_V5_TEMPLATE.copy(),
4784                namespace=namespace,
4785                name=name,
4786            )
4787            return self.sql(result)
4788
4789        return super().uuid_sql(expression)

Generator converts a given syntax tree to the corresponding SQL string.

Arguments:
  • pretty: Whether to format the produced SQL string. Default: False.
  • identify: Determines when an identifier should be quoted. Possible values are: False (default): Never quote, except in cases where it's mandatory by the dialect. True: Always quote except for specials cases. 'safe': Only quote identifiers that are case insensitive.
  • normalize: Whether to normalize identifiers to lowercase. Default: False.
  • pad: The pad size in a formatted string. For example, this affects the indentation of a projection in a query, relative to its nesting level. Default: 2.
  • indent: The indentation size in a formatted string. For example, this affects the indentation of subqueries and filters under a WHERE clause. Default: 2.
  • normalize_functions: How to normalize function names. Possible values are: "upper" or True (default): Convert names to uppercase. "lower": Convert names to lowercase. False: Disables function name normalization.
  • unsupported_level: Determines the generator's behavior when it encounters unsupported expressions. Default ErrorLevel.WARN.
  • max_unsupported: Maximum number of unsupported messages to include in a raised UnsupportedError. This is only relevant if unsupported_level is ErrorLevel.RAISE. Default: 3
  • leading_comma: Whether the comma is leading or trailing in select expressions. This is only relevant when generating in pretty mode. Default: False
  • max_text_width: The max number of characters in a segment before creating new lines in pretty mode. The default is on the smaller end because the length only represents a segment and not the true line length. Default: 80
  • comments: Whether to preserve comments in the output SQL code. Default: True
PARAMETER_TOKEN = '$'
NAMED_PLACEHOLDER_TOKEN = '$'
JOIN_HINTS = False
TABLE_HINTS = False
QUERY_HINTS = False
LIMIT_FETCH = 'LIMIT'
STRUCT_DELIMITER = ('(', ')')
RENAME_TABLE_WITH_DB = False
NVL2_SUPPORTED = False
SEMI_ANTI_JOIN_WITH_SIDE = False
TABLESAMPLE_KEYWORDS = 'USING SAMPLE'
TABLESAMPLE_SEED_KEYWORD = 'REPEATABLE'
LAST_DAY_SUPPORTS_DATE_PART = False
JSON_KEY_VALUE_PAIR_SEP = ','
IGNORE_NULLS_IN_FUNC = True
IGNORE_NULLS_BEFORE_ORDER = False
JSON_PATH_BRACKETED_KEY_SUPPORTED = False
SUPPORTS_CREATE_TABLE_LIKE = False
MULTI_ARG_DISTINCT = False
CAN_IMPLEMENT_ARRAY_ANY = True
SUPPORTS_TO_NUMBER = False
SELECT_KINDS: tuple[str, ...] = ()
SUPPORTS_DECODE_CASE = False
SUPPORTS_DROP_ALTER_ICEBERG_PROPERTY = False
AFTER_HAVING_MODIFIER_TRANSFORMS = {'windows': <function <lambda>>, 'qualify': <function <lambda>>}
SUPPORTS_WINDOW_EXCLUDE = True
COPY_HAS_INTO_KEYWORD = False
STAR_EXCEPT = 'EXCLUDE'
PAD_FILL_PATTERN_IS_REQUIRED = True
ARRAY_SIZE_DIM_REQUIRED: bool | None = False
NORMALIZE_EXTRACT_DATE_PARTS = True
SUPPORTS_LIKE_QUANTIFIERS = False
HISTORICAL_DATA_POST_ALIAS = True
SET_ASSIGNMENT_REQUIRES_VARIABLE_KEYWORD = True
TRANSFORMS = {<class 'sqlglot.expressions.query.JSONPathKey'>: <function <lambda>>, <class 'sqlglot.expressions.query.JSONPathRoot'>: <function <lambda>>, <class 'sqlglot.expressions.query.JSONPathSubscript'>: <function <lambda>>, <class 'sqlglot.expressions.query.JSONPathWildcard'>: <function <lambda>>, <class 'sqlglot.expressions.core.Adjacent'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.AllowedValuesProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.AnalyzeColumns'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.AnalyzeWith'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.array.ArrayContainedBy'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.array.ArrayContainsAll'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.array.ArrayOverlaps'>: <function _array_overlaps_sql>, <class 'sqlglot.expressions.constraints.AssumeColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.AutoRefreshProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.BackupProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.CaseSpecificColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.CalledOnNullInputProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.math.Ceil'>: <function _ceil_floor>, <class 'sqlglot.expressions.constraints.CharacterSetColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.CharacterSetProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.ClusteredColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.CollateColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.CommentColumnConstraint'>: <function no_comment_column_constraint_sql>, <class 'sqlglot.expressions.functions.ConnectByRoot'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.string.ConvertToCharset'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.CopyGrantsProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.CredentialsProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.functions.CurrentCatalog'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.functions.SessionUser'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.DateFormatColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.DefaultColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.ApiProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.ApplicationProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.CatalogProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.ComputeProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.DatabaseProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.DynamicProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.EmptyProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.EncodeColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.EndStatement'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.EnviromentProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.HandlerProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.ParameterStyleProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.EphemeralColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.ExcludeColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.ExecuteAsProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.Except'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.ExternalProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.math.Floor'>: <function _ceil_floor>, <class 'sqlglot.expressions.query.Get'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.GlobalProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.HeapProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.HybridProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.IcebergProperty'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.properties.InheritsProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.InlineLengthColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.InputModelProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.Intersect'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.datatypes.IntervalSpan'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.functions.Int64'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.json.JSONBContainsAnyTopKeys'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.json.JSONBContainsAllTopKeys'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.json.JSONBDeleteAtPath'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.json.JSONBPathExists'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.json.JSONObject'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.json.JSONObjectAgg'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.properties.LanguageProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.LocationProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.LogProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.MaskingProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.MaterializedProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.functions.NetFunc'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.NetworkProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.NonClusteredColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.NoPrimaryIndexProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.NotForReplicationColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.OnCommitProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.OnProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.OnUpdateColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.core.Operator'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.OutputModelProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.core.ExtendsLeft'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.core.ExtendsRight'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.PathColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.PartitionedByBucket'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.PartitionByTruncate'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.core.PivotAny'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.array.PositionalColumn'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.ProjectionPolicyColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.InvisibleColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.ZeroFillColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.Put'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.RemoteWithConnectionModelProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.ReturnsProperty'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.properties.RowAccessProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.core.SafeFunc'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.SampleProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.SecureProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.SecurityIntegrationProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.SetConfigProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.SetProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.SettingsProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.SharingProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.SqlReadWriteProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.SqlSecurityProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.StabilityProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.Stream'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.StreamingTableProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.StrictProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.ddl.SwapTable'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.TableColumn'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.Tags'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.TemporaryProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.TitleColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.array.ToMap'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.ToTableProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.TransformModelProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.TransientProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.VirtualProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.ddl.TriggerExecute'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.Union'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.UnloggedProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.UsingTemplateProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.UsingData'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.UppercaseColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.temporal.UtcDate'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.temporal.UtcTime'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.temporal.UtcTimestamp'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.Variadic'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.array.VarMap'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.ViewAttributeProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.VolatileProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.WithJournalTableProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.WithProcedureOptions'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.WithSchemaBindingProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.WithOperator'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.ForceProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.aggregate.AnyValue'>: <function _anyvalue_sql>, <class 'sqlglot.expressions.core.ApproxDistinct'>: <function approx_count_distinct_sql>, <class 'sqlglot.expressions.math.Boolnot'>: <function _boolnot_sql>, <class 'sqlglot.expressions.math.Booland'>: <function _booland_sql>, <class 'sqlglot.expressions.math.Boolor'>: <function _boolor_sql>, <class 'sqlglot.expressions.array.Array'>: <function preprocess.<locals>._to_sql>, <class 'sqlglot.expressions.array.ArrayAppend'>: <function array_append_sql.<locals>._array_append_sql>, <class 'sqlglot.expressions.array.ArrayCompact'>: <function array_compact_sql>, <class 'sqlglot.expressions.array.ArrayConstructCompact'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.array.ArrayConcat'>: <function array_concat_sql.<locals>._array_concat_sql>, <class 'sqlglot.expressions.array.ArrayContains'>: <function _array_contains_sql>, <class 'sqlglot.expressions.array.ArrayFilter'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.array.ArrayInsert'>: <function _array_insert_sql>, <class 'sqlglot.expressions.array.ArrayPosition'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.array.ArrayRemoveAt'>: <function _array_remove_at_sql>, <class 'sqlglot.expressions.array.ArrayRemove'>: <function remove_from_array_using_filter>, <class 'sqlglot.expressions.array.ArraySort'>: <function _array_sort_sql>, <class 'sqlglot.expressions.array.ArrayPrepend'>: <function array_append_sql.<locals>._array_append_sql>, <class 'sqlglot.expressions.array.ArraySum'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.array.ArrayMax'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.array.ArrayMin'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.string.Base64DecodeBinary'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.string.Base64DecodeString'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.core.BitwiseAnd'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.math.BitwiseAndAgg'>: <function _bitwise_agg_sql>, <class 'sqlglot.expressions.math.BitwiseCount'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.core.BitwiseLeftShift'>: <function _bitshift_sql>, <class 'sqlglot.expressions.core.BitwiseOr'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.math.BitwiseOrAgg'>: <function _bitwise_agg_sql>, <class 'sqlglot.expressions.core.BitwiseRightShift'>: <function _bitshift_sql>, <class 'sqlglot.expressions.math.BitwiseXorAgg'>: <function _bitwise_agg_sql>, <class 'sqlglot.expressions.aggregate.Corr'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.math.CosineDistance'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.CurrentTime'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.functions.CurrentSchemas'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.CurrentTimestamp'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.functions.CurrentVersion'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.Localtime'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.DayOfMonth'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.DayOfWeek'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.DayOfWeekIso'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.DayOfYear'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.Dayname'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.Monthname'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _datatype_sql>, <class 'sqlglot.expressions.temporal.Date'>: <function _date_sql>, <class 'sqlglot.expressions.temporal.DateAdd'>: <function _date_delta_to_binary_interval_op.<locals>._duckdb_date_delta_sql>, <class 'sqlglot.expressions.temporal.DateFromParts'>: <function _date_from_parts_sql>, <class 'sqlglot.expressions.temporal.DateSub'>: <function _date_delta_to_binary_interval_op.<locals>._duckdb_date_delta_sql>, <class 'sqlglot.expressions.temporal.DateDiff'>: <function _date_diff_sql>, <class 'sqlglot.expressions.temporal.DateStrToDate'>: <function datestrtodate_sql>, <class 'sqlglot.expressions.temporal.Datetime'>: <function no_datetime_sql>, <class 'sqlglot.expressions.temporal.DatetimeDiff'>: <function _date_diff_sql>, <class 'sqlglot.expressions.temporal.DatetimeSub'>: <function _date_delta_to_binary_interval_op.<locals>._duckdb_date_delta_sql>, <class 'sqlglot.expressions.temporal.DatetimeAdd'>: <function _date_delta_to_binary_interval_op.<locals>._duckdb_date_delta_sql>, <class 'sqlglot.expressions.temporal.DateToDi'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.string.Decode'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.string.HexDecodeString'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.DiToDate'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.string.Encode'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.functions.EqualNull'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.math.EuclideanDistance'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.GenerateDateArray'>: <function _generate_datetime_array_sql>, <class 'sqlglot.expressions.array.GenerateSeries'>: <function generate_series_sql.<locals>._generate_series_sql>, <class 'sqlglot.expressions.temporal.GenerateTimestampArray'>: <function _generate_datetime_array_sql>, <class 'sqlglot.expressions.math.Getbit'>: <function getbit_sql>, <class 'sqlglot.expressions.aggregate.GroupConcat'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.array.Explode'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.core.IntDiv'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.math.IsInf'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.math.IsNan'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.functions.IsNullValue'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.functions.IsArray'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.json.JSONBExists'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.json.JSONExtract'>: <function _arrow_json_extract_sql>, <class 'sqlglot.expressions.json.JSONExtractArray'>: <function _json_extract_value_array_sql>, <class 'sqlglot.expressions.json.JSONFormat'>: <function _json_format_sql>, <class 'sqlglot.expressions.query.JSONValueArray'>: <function _json_extract_value_array_sql>, <class 'sqlglot.expressions.query.Lateral'>: <function _explode_to_unnest_sql>, <class 'sqlglot.expressions.aggregate.LogicalOr'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.aggregate.LogicalAnd'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.query.Select'>: <function preprocess.<locals>._to_sql>, <class 'sqlglot.expressions.functions.Seq1'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.functions.Seq2'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.functions.Seq4'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.functions.Seq8'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.math.BoolxorAgg'>: <function _boolxor_agg_sql>, <class 'sqlglot.expressions.temporal.MakeInterval'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.string.Initcap'>: <function _initcap_sql>, <class 'sqlglot.expressions.string.MD5Digest'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.string.SHA'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.string.SHA1Digest'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.string.SHA2'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.string.SHA2Digest'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.MonthsBetween'>: <function months_between_sql>, <class 'sqlglot.expressions.temporal.NextDay'>: <function _day_navigation_sql>, <class 'sqlglot.expressions.aggregate.PercentileCont'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.aggregate.PercentileDisc'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.query.Pivot'>: <function preprocess.<locals>._to_sql>, <class 'sqlglot.expressions.temporal.PreviousDay'>: <function _day_navigation_sql>, <class 'sqlglot.expressions.string.RegexpILike'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.string.RegexpSplit'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.aggregate.RegrValx'>: <function _regr_val_sql>, <class 'sqlglot.expressions.aggregate.RegrValy'>: <function _regr_val_sql>, <class 'sqlglot.expressions.query.Return'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.StrToUnix'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.array.Struct'>: <function _struct_sql>, <class 'sqlglot.expressions.array.Transform'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.TimeAdd'>: <function _date_delta_to_binary_interval_op.<locals>._duckdb_date_delta_sql>, <class 'sqlglot.expressions.temporal.TimeSub'>: <function _date_delta_to_binary_interval_op.<locals>._duckdb_date_delta_sql>, <class 'sqlglot.expressions.temporal.Time'>: <function no_time_sql>, <class 'sqlglot.expressions.temporal.TimeDiff'>: <function _timediff_sql>, <class 'sqlglot.expressions.temporal.Timestamp'>: <function no_timestamp_sql>, <class 'sqlglot.expressions.temporal.TimestampAdd'>: <function _date_delta_to_binary_interval_op.<locals>._duckdb_date_delta_sql>, <class 'sqlglot.expressions.temporal.TimestampDiff'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.TimestampSub'>: <function _date_delta_to_binary_interval_op.<locals>._duckdb_date_delta_sql>, <class 'sqlglot.expressions.temporal.TimeStrToDate'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.TimeStrToTime'>: <function timestrtotime_sql>, <class 'sqlglot.expressions.temporal.TimeStrToUnix'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.TimeToStr'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.functions.ToBoolean'>: <function _to_boolean_sql>, <class 'sqlglot.expressions.functions.ToVariant'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.TimeToUnix'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.TsOrDiToDi'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.TsOrDsAdd'>: <function _date_delta_to_binary_interval_op.<locals>._duckdb_date_delta_sql>, <class 'sqlglot.expressions.temporal.TsOrDsDiff'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.UnixMicros'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.UnixMillis'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.UnixSeconds'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.UnixToStr'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.UnixToTime'>: <function _unix_to_time_sql>, <class 'sqlglot.expressions.temporal.UnixToTimeStr'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.aggregate.VariancePop'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.WeekOfYear'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.YearOfWeek'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.YearOfWeekIso'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.core.Xor'>: <function _xor_sql>, <class 'sqlglot.expressions.json.JSONBObjectAgg'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.DateBin'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.LastDay'>: <function _last_day_sql>}
TYPE_MAPPING = {<DType.DATETIME2: 'DATETIME2'>: 'TIMESTAMP', <DType.NCHAR: 'NCHAR'>: 'TEXT', <DType.NVARCHAR: 'NVARCHAR'>: 'TEXT', <DType.MEDIUMTEXT: 'MEDIUMTEXT'>: 'TEXT', <DType.LONGTEXT: 'LONGTEXT'>: 'TEXT', <DType.TINYTEXT: 'TINYTEXT'>: 'TEXT', <DType.BLOB: 'BLOB'>: 'VARBINARY', <DType.MEDIUMBLOB: 'MEDIUMBLOB'>: 'BLOB', <DType.LONGBLOB: 'LONGBLOB'>: 'BLOB', <DType.TINYBLOB: 'TINYBLOB'>: 'BLOB', <DType.INET: 'INET'>: 'INET', <DType.ROWVERSION: 'ROWVERSION'>: 'BLOB', <DType.SMALLDATETIME: 'SMALLDATETIME'>: 'TIMESTAMP', <DType.BINARY: 'BINARY'>: 'BLOB', <DType.BPCHAR: 'BPCHAR'>: 'TEXT', <DType.CHAR: 'CHAR'>: 'TEXT', <DType.DATETIME: 'DATETIME'>: 'TIMESTAMP', <DType.DECFLOAT: 'DECFLOAT'>: 'DECIMAL', <DType.FLOAT: 'FLOAT'>: 'REAL', <DType.JSONB: 'JSONB'>: 'JSON', <DType.UINT: 'UINT'>: 'UINTEGER', <DType.VARBINARY: 'VARBINARY'>: 'BLOB', <DType.VARCHAR: 'VARCHAR'>: 'TEXT', <DType.TIMESTAMPLTZ: 'TIMESTAMPLTZ'>: 'TIMESTAMPTZ', <DType.TIMESTAMPNTZ: 'TIMESTAMPNTZ'>: 'TIMESTAMP', <DType.TIMESTAMP_S: 'TIMESTAMP_S'>: 'TIMESTAMP_S', <DType.TIMESTAMP_MS: 'TIMESTAMP_MS'>: 'TIMESTAMP_MS', <DType.TIMESTAMP_NS: 'TIMESTAMP_NS'>: 'TIMESTAMP_NS', <DType.BIGDECIMAL: 'BIGDECIMAL'>: 'DECIMAL'}
TYPE_PARAM_SETTINGS = {<DType.BIGDECIMAL: 'BIGDECIMAL'>: ((38, 5), (38, 38)), <DType.DECFLOAT: 'DECFLOAT'>: ((38, 5), (38, 38))}
RESERVED_KEYWORDS = {'from', 'cast', 'lateral_p', 'and', 'for', 'initially', 'window', 'unique', 'placing', 'both', 'asc_p', 'limit', 'not', 'leading', 'localtime', 'where', 'in_p', 'returning', 'session_user', 'default', 'group_p', 'references', 'any', 'check_p', 'primary', 'union', 'analyze', 'all', 'grant', 'current_user', 'except', 'deferrable', 'current_date', 'having', 'offset', 'variadic', 'false_p', 'else', 'order', 'some', 'as', 'constraint', 'do', 'foreign', 'current_timestamp', 'on', 'to', 'localtimestamp', 'when', 'true_p', 'fetch', 'array', 'distinct', 'intersect', 'asymmetric', 'null_p', 'current_catalog', 'end_p', 'current_time', 'into', 'collate', 'analyse', 'table', 'create_p', 'symmetric', 'only', 'trailing', 'user', 'with', 'case', 'using', 'then', 'or', 'desc_p', 'select', 'current_role', 'column'}
UNWRAPPED_INTERVAL_VALUES = (<class 'sqlglot.expressions.core.Literal'>, <class 'sqlglot.expressions.core.Paren'>)
PROPERTIES_LOCATION = {<class 'sqlglot.expressions.properties.AllowedValuesProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.AlgorithmProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.ApiProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.ApplicationProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.AutoIncrementProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.AutoRefreshProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.BackupProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.BlockCompressionProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.CalledOnNullInputProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.CatalogProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.CharacterSetProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.ChecksumProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.CollateProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.ComputeProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.CopyGrantsProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.query.Cluster'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.ClusteredByProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.ClusterProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.DistributedByProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.DuplicateKeyProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.DataBlocksizeProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.DatabaseProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.DataDeletionProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.DefinerProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.DictRange'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.DictProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.DynamicProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.DistKeyProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.DistStyleProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.EmptyProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.EncodeProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.EngineProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.EnviromentProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.HandlerProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.ParameterStyleProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.ExecuteAsProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.ExternalProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.FallbackProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.FileFormatProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.FreespaceProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.GlobalProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.HeapProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.HybridProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.InheritsProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.IcebergProperty'>: <PropertiesLocation.POST_CREATE: 'POST_CREATE'>, <class 'sqlglot.expressions.properties.IncludeProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.InputModelProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.IsolatedLoadingProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.JournalProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.LanguageProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.LikeProperty'>: <PropertiesLocation.POST_SCHEMA: 'POST_SCHEMA'>, <class 'sqlglot.expressions.properties.LocationProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.LockProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.LockingProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.LogProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.MaskingProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.MaterializedProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.MergeBlockRatioProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.ModuleProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.NetworkProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.NoPrimaryIndexProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.OnProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.OnCommitProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.query.Order'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.OutputModelProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.PartitionedByProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.PartitionedOfProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.constraints.PrimaryKey'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.Property'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.RefreshTriggerProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.RemoteWithConnectionModelProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.ReturnsProperty'>: <PropertiesLocation.POST_ALIAS: 'POST_ALIAS'>, <class 'sqlglot.expressions.properties.RollupProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.RowAccessProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.RowFormatProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.RowFormatDelimitedProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.RowFormatSerdeProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.SampleProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.SchemaCommentProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.SecureProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.SecurityIntegrationProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.SerdeProperties'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.ddl.Set'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.SettingsProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.SetProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.SetConfigProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.SharingProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.ddl.SequenceProperties'>: <PropertiesLocation.POST_EXPRESSION: 'POST_EXPRESSION'>, <class 'sqlglot.expressions.ddl.TriggerProperties'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.SortKeyProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.SqlReadWriteProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.SqlSecurityProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.StabilityProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.StorageHandlerProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.StreamingTableProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.StrictProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.Tags'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.TemporaryProperty'>: <PropertiesLocation.POST_CREATE: 'POST_CREATE'>, <class 'sqlglot.expressions.properties.ToTableProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.TransientProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.TransformModelProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.ddl.MergeTreeTTL'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.UnloggedProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.UsingProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.UsingTemplateProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.ViewAttributeProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.VirtualProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.VolatileProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.WithDataProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.WithJournalTableProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.WithProcedureOptions'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.WithSchemaBindingProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.WithSystemVersioningProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>, <class 'sqlglot.expressions.properties.ForceProperty'>: <PropertiesLocation.UNSUPPORTED: 'UNSUPPORTED'>}
ZIPF_TEMPLATE: sqlglot.expressions.core.Expr = Select( expressions=[ Min( this=Column( this=Identifier(this=i, quoted=False)))], from_=From( this=Table( this=Identifier(this=cdf, quoted=False))), where=Where( this=GTE( this=Column( this=Identifier(this=p, quoted=False)), expression=Subquery( this=Select( expressions=[ Column( this=Identifier(this=r, quoted=False))], from_=From( this=Table( this=Identifier(this=rand, quoted=False))))))), with_=With( expressions=[ CTE( this=Select( expressions=[ Alias( this=Placeholder(this=random_expr), alias=Identifier(this=r, quoted=False))]), alias=TableAlias( this=Identifier(this=rand, quoted=False))), CTE( this=Select( expressions=[ Column( this=Identifier(this=i, quoted=False)), Alias( this=Div( this=Literal(this=1.0, is_string=False), expression=Pow( this=Column( this=Identifier(this=i, quoted=False)), expression=Placeholder(this=s)), typed=False, safe=False), alias=Identifier(this=w, quoted=False))], from_=From( this=Table( this=Anonymous( this=RANGE, expressions=[ Literal(this=1, is_string=False), Add( this=Placeholder(this=n), expression=Literal(this=1, is_string=False))]), alias=TableAlias( this=Identifier(this=t, quoted=False), columns=[ Identifier(this=i, quoted=False)])))), alias=TableAlias( this=Identifier(this=weights, quoted=False))), CTE( this=Select( expressions=[ Column( this=Identifier(this=i, quoted=False)), Alias( this=Div( this=Window( this=Sum( this=Column( this=Identifier(this=w, quoted=False))), order=Order( expressions=[ Ordered( this=Column( this=Identifier(this=i, quoted=False)), nulls_first=True)]), over=OVER), expression=Window( this=Sum( this=Column( this=Identifier(this=w, quoted=False))), over=OVER), typed=False, safe=False), alias=Identifier(this=p, quoted=False))], from_=From( this=Table( this=Identifier(this=weights, quoted=False)))), alias=TableAlias( this=Identifier(this=cdf, quoted=False)))]))
NORMAL_TEMPLATE: sqlglot.expressions.core.Expr = Add( this=Placeholder(this=mean), expression=Paren( this=Mul( this=Mul( this=Placeholder(this=stddev), expression=Sqrt( this=Mul( this=Neg( this=Literal(this=2, is_string=False)), expression=Ln( this=Greatest( this=Placeholder(this=u1), expressions=[ Literal(this=1e-10, is_string=False)], ignore_nulls=True))))), expression=Cos( this=Mul( this=Mul( this=Literal(this=2, is_string=False), expression=Pi()), expression=Placeholder(this=u2))))))
SEEDED_RANDOM_TEMPLATE: sqlglot.expressions.core.Expr = Div( this=Paren( this=Mod( this=Abs( this=Anonymous( this=HASH, expressions=[ Placeholder(this=seed)])), expression=Literal(this=1000000, is_string=False))), expression=Literal(this=1000000.0, is_string=False), typed=False, safe=False)
SEQ_UNSIGNED: sqlglot.expressions.core.Expr = Mod( this=Placeholder(this=base), expression=Placeholder(this=max_val))
SEQ_SIGNED: sqlglot.expressions.core.Expr = Paren( this=Case( ifs=[ If( this=GTE( this=Mod( this=Placeholder(this=base), expression=Placeholder(this=max_val)), expression=Placeholder(this=half)), true=Sub( this=Mod( this=Placeholder(this=base), expression=Placeholder(this=max_val)), expression=Placeholder(this=max_val)))], default=Mod( this=Placeholder(this=base), expression=Placeholder(this=max_val))))
MAPCAT_TEMPLATE: sqlglot.expressions.core.Expr = Case( ifs=[ If( this=Or( this=Is( this=Placeholder(this=map1), expression=Null()), expression=Is( this=Placeholder(this=map2), expression=Null())), true=Null())], default=MapFromEntries( this=Anonymous( this=LIST_FILTER, expressions=[ Anonymous( this=LIST_TRANSFORM, expressions=[ Anonymous( this=LIST_DISTINCT, expressions=[ Anonymous( this=LIST_CONCAT, expressions=[ MapKeys( this=Placeholder(this=map1)), MapKeys( this=Placeholder(this=map2))])]), Lambda( this=Anonymous( this=STRUCT_PACK, expressions=[ PropertyEQ( this=Identifier(this=key, quoted=False), expression=Identifier(this=__k, quoted=False)), PropertyEQ( this=Identifier(this=value, quoted=False), expression=Coalesce( this=Bracket( this=Placeholder(this=map2), expressions=[ Identifier(this=__k, quoted=False)]), expressions=[ Bracket( this=Placeholder(this=map1), expressions=[ Identifier(this=__k, quoted=False)])]))]), expressions=[ Identifier(this=__k, quoted=False)])]), Lambda( this=Not( this=Is( this=Dot( this=Identifier(this=__x, quoted=False), expression=Identifier(this=value, quoted=False)), expression=Null())), expressions=[ Identifier(this=__x, quoted=False)])])))
EXTRACT_STRFTIME_MAPPINGS: dict[str, tuple[str, str]] = {'WEEKISO': ('%V', 'INTEGER'), 'YEAROFWEEK': ('%G', 'INTEGER'), 'YEAROFWEEKISO': ('%G', 'INTEGER'), 'NANOSECOND': ('%n', 'BIGINT')}
EXTRACT_EPOCH_MAPPINGS: dict[str, str] = {'EPOCH_SECOND': 'EPOCH', 'EPOCH_MILLISECOND': 'EPOCH_MS', 'EPOCH_MICROSECOND': 'EPOCH_US', 'EPOCH_NANOSECOND': 'EPOCH_NS'}
BITMAP_CONSTRUCT_AGG_TEMPLATE: sqlglot.expressions.core.Expr = Select( expressions=[ Case( ifs=[ If( this=Or( this=Is( this=Column( this=Identifier(this=l, quoted=False)), expression=Null()), expression=EQ( this=Length( this=Column( this=Identifier(this=l, quoted=False))), expression=Literal(this=0, is_string=False))), true=Null()), If( this=NEQ( this=Length( this=Column( this=Identifier(this=l, quoted=False))), expression=Length( this=Anonymous( this=LIST_FILTER, expressions=[ Column( this=Identifier(this=l, quoted=False)), Lambda( this=Between( this=Identifier(this=__v, quoted=False), low=Literal(this=0, is_string=False), high=Literal(this=32767, is_string=False)), expressions=[ Identifier(this=__v, quoted=False)])]))), true=Null()), If( this=LT( this=Length( this=Column( this=Identifier(this=l, quoted=False))), expression=Literal(this=5, is_string=False)), true=Unhex( this=DPipe( this=DPipe( this=Anonymous( this=PRINTF, expressions=[ Literal(this='%04X', is_string=True), Length( this=Column( this=Identifier(this=l, quoted=False)))]), expression=Column( this=Identifier(this=h, quoted=False)), safe=True), expression=Repeat( this=Literal(this='00', is_string=True), times=Mul( this=Greatest( this=Literal(this=0, is_string=False), expressions=[ Sub( this=Literal(this=4, is_string=False), expression=Length( this=Column( this=Identifier(this=l, quoted=False))))], ignore_nulls=True), expression=Literal(this=2, is_string=False))), safe=True)))], default=Unhex( this=DPipe( this=Literal(this='08000000000000000000', is_string=True), expression=Column( this=Identifier(this=h, quoted=False)), safe=True)))], from_=From( this=Subquery( this=Select( expressions=[ Column( this=Identifier(this=l, quoted=False)), Alias( this=Coalesce( this=Anonymous( this=LIST_REDUCE, expressions=[ Anonymous( this=LIST_TRANSFORM, expressions=[ Column( this=Identifier(this=l, quoted=False)), Lambda( this=Anonymous( this=PRINTF, expressions=[ Literal(this='%02X%02X', is_string=True), BitwiseAnd( this=Cast( this=Identifier(this=__x, quoted=False), to=DataType(this=DType.INT, nested=False), _type=DataType(this=DType.INT, nested=False)), expression=Literal(this=255, is_string=False)), BitwiseAnd( this=Paren( this=BitwiseRightShift( this=Cast( this=Identifier(this=__x, quoted=False), to=DataType(this=DType.INT, nested=False), _type=DataType(this=DType.INT, nested=False)), expression=Literal(this=8, is_string=False))), expression=Literal(this=255, is_string=False))]), expressions=[ Identifier(this=__x, quoted=False)])]), Lambda( this=DPipe( this=Identifier(this=__a, quoted=False), expression=Identifier(this=__b, quoted=False), safe=True), expressions=[ Identifier(this=__a, quoted=False), Identifier(this=__b, quoted=False)]), Literal(this='', is_string=True)]), expressions=[ Literal(this='', is_string=True)]), alias=Identifier(this=h, quoted=False))], from_=From( this=Subquery( this=Select( expressions=[ Alias( this=Anonymous( this=LIST_SORT, expressions=[ Anonymous( this=LIST_DISTINCT, expressions=[ Filter( this=List( expressions=[ Placeholder(this=arg)]), expression=Where( this=Not( this=Is( this=Placeholder(this=arg), expression=Null()))))])]), alias=Identifier(this=l, quoted=False))])))))))
RANDSTR_TEMPLATE: sqlglot.expressions.core.Expr = Select( expressions=[ Anonymous( this=LISTAGG, expressions=[ Substring( this=Literal(this='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', is_string=True), start=Add( this=Literal(this=1, is_string=False), expression=Cast( this=Floor( this=Mul( this=Column( this=Identifier(this=random_value, quoted=False)), expression=Literal(this=62, is_string=False))), to=DataType(this=DType.INT, nested=False), _type=DataType(this=DType.INT, nested=False))), length=Literal(this=1, is_string=False)), Literal(this='', is_string=True)])], from_=From( this=Subquery( this=Select( expressions=[ Alias( this=Div( this=Paren( this=Mod( this=Abs( this=Anonymous( this=HASH, expressions=[ Add( this=Column( this=Identifier(this=i, quoted=False)), expression=Placeholder(this=seed))])), expression=Literal(this=1000, is_string=False))), expression=Literal(this=1000.0, is_string=False), typed=False, safe=False), alias=Identifier(this=random_value, quoted=False))], from_=From( this=Table( this=Anonymous( this=RANGE, expressions=[ Placeholder(this=length)]), alias=TableAlias( this=Identifier(this=t, quoted=False), columns=[ Identifier(this=i, quoted=False)])))))))
MINHASH_TEMPLATE: sqlglot.expressions.core.Expr = Select( expressions=[ JSONObject( expressions=[ JSONKeyValue( this=Literal(this='state', is_string=True), expression=List( expressions=[ Order( this=Column( this=Identifier(this=min_h, quoted=False)), expressions=[ Ordered( this=Column( this=Identifier(this=seed, quoted=False)), nulls_first=True)])])), JSONKeyValue( this=Literal(this='type', is_string=True), expression=Literal(this='minhash', is_string=True)), JSONKeyValue( this=Literal(this='version', is_string=True), expression=Literal(this=1, is_string=False))], return_type=False, encoding=False)], from_=From( this=Subquery( this=Select( expressions=[ Column( this=Identifier(this=seed, quoted=False)), Alias( this=Anonymous( this=LIST_MIN, expressions=[ Anonymous( this=LIST_TRANSFORM, expressions=[ Column( this=Identifier(this=vals, quoted=False)), Lambda( this=Anonymous( this=HASH, expressions=[ DPipe( this=Cast( this=Identifier(this=__v, quoted=False), to=DataType(this=DType.VARCHAR, nested=False), _type=DataType(this=DType.VARCHAR, nested=False)), expression=Cast( this=Column( this=Identifier(this=seed, quoted=False)), to=DataType(this=DType.VARCHAR, nested=False), _type=DataType(this=DType.VARCHAR, nested=False)), safe=True)]), expressions=[ Identifier(this=__v, quoted=False)])])]), alias=Identifier(this=min_h, quoted=False))], from_=From( this=Subquery( this=Select( expressions=[ Alias( this=List( expressions=[ Placeholder(this=expr)]), alias=Identifier(this=vals, quoted=False))]))), joins=[ Join( this=Table( this=Anonymous( this=RANGE, expressions=[ Literal(this=0, is_string=False), Placeholder(this=k)]), alias=TableAlias( this=Identifier(this=t, quoted=False), columns=[ Identifier(this=seed, quoted=False)])))]))))
MINHASH_COMBINE_TEMPLATE: sqlglot.expressions.core.Expr = Select( expressions=[ JSONObject( expressions=[ JSONKeyValue( this=Literal(this='state', is_string=True), expression=List( expressions=[ Order( this=Column( this=Identifier(this=min_h, quoted=False)), expressions=[ Ordered( this=Column( this=Identifier(this=idx, quoted=False)), nulls_first=True)])])), JSONKeyValue( this=Literal(this='type', is_string=True), expression=Literal(this='minhash', is_string=True)), JSONKeyValue( this=Literal(this='version', is_string=True), expression=Literal(this=1, is_string=False))], return_type=False, encoding=False)], from_=From( this=Subquery( this=Select( expressions=[ Alias( this=Column( this=Identifier(this=pos, quoted=False)), alias=Identifier(this=idx, quoted=False)), Alias( this=Min( this=Column( this=Identifier(this=val, quoted=False))), alias=Identifier(this=min_h, quoted=False))], from_=From( this=Unnest( expressions=[ List( expressions=[ Placeholder(this=expr)])], alias=TableAlias( this=Identifier(this=_, quoted=False), columns=[ Identifier(this=sig, quoted=False)]), offset=False)), joins=[ Join( this=Unnest( expressions=[ Cast( this=JSONExtract( this=Column( this=Identifier(this=sig, quoted=False)), expression=JSONPath( expressions=[ JSONPathRoot(), JSONPathKey(this=state)]), only_json_types=False), to=DataType( this=DType.ARRAY, expressions=[ DataType(this=DType.USERDEFINED, kind=UBIGINT)], nested=True), _type=DataType( this=DType.ARRAY, expressions=[ DataType(this=DType.USERDEFINED, kind=UBIGINT)], nested=True))], alias=TableAlias( this=Identifier(this=t, quoted=False), columns=[ Identifier(this=val, quoted=False)]), offset=Identifier(this=pos, quoted=False)))], group=Group( expressions=[ Column( this=Identifier(this=pos, quoted=False))])))))
APPROXIMATE_SIMILARITY_TEMPLATE: sqlglot.expressions.core.Expr = Select( expressions=[ Div( this=Cast( this=Sum( this=Case( ifs=[ If( this=EQ( this=Column( this=Identifier(this=num_distinct, quoted=False)), expression=Literal(this=1, is_string=False)), true=Literal(this=1, is_string=False))], default=Literal(this=0, is_string=False))), to=DataType(this=DType.DOUBLE, nested=False), _type=DataType(this=DType.DOUBLE, nested=False)), expression=Count( this=Star(), big_int=True), typed=False, safe=False)], from_=From( this=Subquery( this=Select( expressions=[ Column( this=Identifier(this=pos, quoted=False)), Alias( this=Count( this=Distinct( expressions=[ Column( this=Identifier(this=h, quoted=False))]), big_int=True), alias=Identifier(this=num_distinct, quoted=False))], from_=From( this=Subquery( this=Select( expressions=[ Column( this=Identifier(this=h, quoted=False)), Column( this=Identifier(this=pos, quoted=False))], from_=From( this=Unnest( expressions=[ List( expressions=[ Placeholder(this=expr)])], alias=TableAlias( this=Identifier(this=_, quoted=False), columns=[ Identifier(this=sig, quoted=False)]), offset=False)), joins=[ Join( this=Unnest( expressions=[ Cast( this=JSONExtract( this=Column( this=Identifier(this=sig, quoted=False)), expression=JSONPath( expressions=[ JSONPathRoot(), JSONPathKey(this=state)]), only_json_types=False), to=DataType( this=DType.ARRAY, expressions=[ DataType(this=DType.USERDEFINED, kind=UBIGINT)], nested=True), _type=DataType( this=DType.ARRAY, expressions=[ DataType(this=DType.USERDEFINED, kind=UBIGINT)], nested=True))], alias=TableAlias( this=Identifier(this=s, quoted=False), columns=[ Identifier(this=h, quoted=False)]), offset=Identifier(this=pos, quoted=False)))]))), group=Group( expressions=[ Column( this=Identifier(this=pos, quoted=False))])))))
ARRAYS_ZIP_TEMPLATE: sqlglot.expressions.core.Expr = Case( ifs=[ If( this=Placeholder(this=null_check), true=Null()), If( this=Placeholder(this=all_empty_check), true=Array( expressions=[ Placeholder(this=empty_struct)]))], default=Anonymous( this=LIST_TRANSFORM, expressions=[ Anonymous( this=RANGE, expressions=[ Literal(this=0, is_string=False), Placeholder(this=max_len)]), Lambda( this=Placeholder(this=transform_struct), expressions=[ Identifier(this=__i, quoted=False)])]))
UUID_V5_TEMPLATE: sqlglot.expressions.core.Expr = Subquery( this=Select( expressions=[ Lower( this=DPipe( this=DPipe( this=DPipe( this=DPipe( this=DPipe( this=DPipe( this=DPipe( this=DPipe( this=DPipe( this=DPipe( this=Substring( this=Column( this=Identifier(this=h, quoted=False)), start=Literal(this=1, is_string=False), length=Literal(this=8, is_string=False)), expression=Literal(this='-', is_string=True), safe=True), expression=Substring( this=Column( this=Identifier(this=h, quoted=False)), start=Literal(this=9, is_string=False), length=Literal(this=4, is_string=False)), safe=True), expression=Literal(this='-', is_string=True), safe=True), expression=Literal(this='5', is_string=True), safe=True), expression=Substring( this=Column( this=Identifier(this=h, quoted=False)), start=Literal(this=14, is_string=False), length=Literal(this=3, is_string=False)), safe=True), expression=Literal(this='-', is_string=True), safe=True), expression=Format( this=Literal(this='{:02x}', is_string=True), expressions=[ BitwiseOr( this=BitwiseAnd( this=Cast( this=DPipe( this=Literal(this='0x', is_string=True), expression=Substring( this=Column( this=Identifier(this=h, quoted=False)), start=Literal(this=17, is_string=False), length=Literal(this=2, is_string=False)), safe=True), to=DataType(this=DType.INT, nested=False), _type=DataType(this=DType.INT, nested=False)), expression=Literal(this=63, is_string=False)), expression=Literal(this=128, is_string=False))]), safe=True), expression=Substring( this=Column( this=Identifier(this=h, quoted=False)), start=Literal(this=19, is_string=False), length=Literal(this=2, is_string=False)), safe=True), expression=Literal(this='-', is_string=True), safe=True), expression=Substring( this=Column( this=Identifier(this=h, quoted=False)), start=Literal(this=21, is_string=False), length=Literal(this=12, is_string=False)), safe=True))], from_=From( this=Subquery( this=Select( expressions=[ Alias( this=Substring( this=SHA( this=DPipe( this=Unhex( this=Replace( this=Placeholder(this=namespace), expression=Literal(this='-', is_string=True), replacement=Literal(this='', is_string=True))), expression=Encode( this=Placeholder(this=name), charset=Literal(this='utf8', is_string=True)), safe=True)), start=Literal(this=1, is_string=False), length=Literal(this=32, is_string=False)), alias=Identifier(this=h, quoted=False))])))))
ARRAY_BAG_TEMPLATE: sqlglot.expressions.core.Expr = Case( ifs=[ If( this=Or( this=Is( this=Placeholder(this=arr1), expression=Null()), expression=Is( this=Placeholder(this=arr2), expression=Null())), true=Null())], default=Anonymous( this=LIST_TRANSFORM, expressions=[ Anonymous( this=LIST_FILTER, expressions=[ Anonymous( this=LIST_ZIP, expressions=[ Placeholder(this=arr1), GenerateSeries( start=Literal(this=1, is_string=False), end=Length( this=Placeholder(this=arr1)))]), Lambda( this=Placeholder(this=cond), expressions=[ Identifier(this=pair, quoted=False)])]), Lambda( this=Bracket( this=Identifier(this=pair, quoted=False), expressions=[ Literal(this=0, is_string=False)]), expressions=[ Identifier(this=pair, quoted=False)])]))
ARRAY_EXCEPT_CONDITION: sqlglot.expressions.core.Expr = GT( this=Length( this=Anonymous( this=LIST_FILTER, expressions=[ Bracket( this=Placeholder(this=arr1), expressions=[ Slice( this=Literal(this=1, is_string=False), expression=Bracket( this=Column( this=Identifier(this=pair, quoted=False)), expressions=[ Literal(this=1, is_string=False)]))]), Lambda( this=NullSafeEQ( this=Identifier(this=e, quoted=False), expression=Bracket( this=Column( this=Identifier(this=pair, quoted=False)), expressions=[ Literal(this=0, is_string=False)])), expressions=[ Identifier(this=e, quoted=False)])])), expression=Length( this=Anonymous( this=LIST_FILTER, expressions=[ Placeholder(this=arr2), Lambda( this=NullSafeEQ( this=Identifier(this=e, quoted=False), expression=Bracket( this=Column( this=Identifier(this=pair, quoted=False)), expressions=[ Literal(this=0, is_string=False)])), expressions=[ Identifier(this=e, quoted=False)])])))
ARRAY_INTERSECTION_CONDITION: sqlglot.expressions.core.Expr = LTE( this=Length( this=Anonymous( this=LIST_FILTER, expressions=[ Bracket( this=Placeholder(this=arr1), expressions=[ Slice( this=Literal(this=1, is_string=False), expression=Bracket( this=Column( this=Identifier(this=pair, quoted=False)), expressions=[ Literal(this=1, is_string=False)]))]), Lambda( this=NullSafeEQ( this=Identifier(this=e, quoted=False), expression=Bracket( this=Column( this=Identifier(this=pair, quoted=False)), expressions=[ Literal(this=0, is_string=False)])), expressions=[ Identifier(this=e, quoted=False)])])), expression=Length( this=Anonymous( this=LIST_FILTER, expressions=[ Placeholder(this=arr2), Lambda( this=NullSafeEQ( this=Identifier(this=e, quoted=False), expression=Bracket( this=Column( this=Identifier(this=pair, quoted=False)), expressions=[ Literal(this=0, is_string=False)])), expressions=[ Identifier(this=e, quoted=False)])])))
ARRAY_EXCEPT_SET_TEMPLATE: sqlglot.expressions.core.Expr = Case( ifs=[ If( this=Or( this=Is( this=Placeholder(this=arr1), expression=Null()), expression=Is( this=Placeholder(this=arr2), expression=Null())), true=Null())], default=Anonymous( this=LIST_FILTER, expressions=[ Anonymous( this=LIST_DISTINCT, expressions=[ Placeholder(this=arr1)]), Lambda( this=EQ( this=Length( this=Anonymous( this=LIST_FILTER, expressions=[ Placeholder(this=arr2), Lambda( this=NullSafeEQ( this=Identifier(this=x, quoted=False), expression=Identifier(this=e, quoted=False)), expressions=[ Identifier(this=x, quoted=False)])])), expression=Literal(this=0, is_string=False)), expressions=[ Identifier(this=e, quoted=False)])]))
IN_UNNEST_TEMPLATE: sqlglot.expressions.core.Expr = Case( ifs=[ If( this=Or( this=Is( this=Placeholder(this=arr), expression=Null()), expression=EQ( this=ArraySize( this=Placeholder(this=arr)), expression=Literal(this=0, is_string=False))), true=Boolean(this=False)), If( this=ArrayContains( this=Placeholder(this=arr), expression=Placeholder(this=value)), true=Boolean(this=True)), If( this=Or( this=Is( this=Placeholder(this=value), expression=Null()), expression=NEQ( this=ArraySize( this=Placeholder(this=arr)), expression=Anonymous( this=LIST_COUNT, expressions=[ Placeholder(this=arr)]))), true=Null())], default=Boolean(this=False))
STRTOK_TO_ARRAY_TEMPLATE: sqlglot.expressions.core.Expr = Case( ifs=[ If( this=Is( this=Placeholder(this=delimiter), expression=Null()), true=Null())], default=Anonymous( this=LIST_FILTER, expressions=[ Anonymous( this=REGEXP_SPLIT_TO_ARRAY, expressions=[ Placeholder(this=string), Case( ifs=[ If( this=EQ( this=Placeholder(this=delimiter), expression=Literal(this='', is_string=True)), true=Literal(this='.^', is_string=True))], default=Concat( expressions=[ Literal(this='[', is_string=True), Placeholder(this=escaped), Literal(this=']', is_string=True)], safe=True, coalesce=False))]), Lambda( this=Not( this=EQ( this=Identifier(this=x, quoted=False), expression=Literal(this='', is_string=True))), expressions=[ Identifier(this=x, quoted=False)])]))
STRTOK_TEMPLATE: sqlglot.expressions.core.Expr = Case( ifs=[ If( this=And( this=EQ( this=Placeholder(this=delimiter), expression=Literal(this='', is_string=True)), expression=EQ( this=Placeholder(this=string), expression=Literal(this='', is_string=True))), true=Null()), If( this=And( this=EQ( this=Placeholder(this=delimiter), expression=Literal(this='', is_string=True)), expression=EQ( this=Placeholder(this=part_index), expression=Literal(this=1, is_string=False))), true=Placeholder(this=string)), If( this=EQ( this=Placeholder(this=delimiter), expression=Literal(this='', is_string=True)), true=Null()), If( this=LT( this=Placeholder(this=part_index), expression=Literal(this=0, is_string=False)), true=Null()), If( this=Or( this=Or( this=Is( this=Placeholder(this=string), expression=Null()), expression=Is( this=Placeholder(this=delimiter), expression=Null())), expression=Is( this=Placeholder(this=part_index), expression=Null())), true=Null())], default=Placeholder(this=base_func))
def timeslice_sql(self, expression: sqlglot.expressions.temporal.TimeSlice) -> str:
2316    def timeslice_sql(self, expression: exp.TimeSlice) -> str:
2317        """
2318        Transform Snowflake's TIME_SLICE to DuckDB's time_bucket.
2319
2320        Snowflake: TIME_SLICE(date_expr, slice_length, 'UNIT' [, 'START'|'END'])
2321        DuckDB:    time_bucket(INTERVAL 'slice_length' UNIT, date_expr)
2322
2323        For 'END' kind, add the interval to get the end of the slice.
2324        For DATE type with 'END', cast result back to DATE to preserve type.
2325        """
2326        date_expr = expression.this
2327        slice_length = expression.expression
2328        unit = expression.unit
2329        kind = expression.text("kind").upper()
2330
2331        # Create INTERVAL expression: INTERVAL 'N' UNIT
2332        interval_expr = exp.Interval(this=slice_length, unit=unit)
2333
2334        # Create base time_bucket expression
2335        time_bucket_expr = exp.func("time_bucket", interval_expr, date_expr)
2336
2337        # Check if we need the end of the slice (default is start)
2338        if not kind == "END":
2339            # For 'START', return time_bucket directly
2340            return self.sql(time_bucket_expr)
2341
2342        # For 'END', add the interval to get end of slice
2343        add_expr = exp.Add(this=time_bucket_expr, expression=interval_expr.copy())
2344
2345        # If input is DATE type, cast result back to DATE to preserve type
2346        # DuckDB converts DATE to TIMESTAMP when adding intervals
2347        if date_expr.is_type(exp.DType.DATE):
2348            return self.sql(exp.cast(add_expr, exp.DType.DATE))
2349
2350        return self.sql(add_expr)

Transform Snowflake's TIME_SLICE to DuckDB's time_bucket.

Snowflake: TIME_SLICE(date_expr, slice_length, 'UNIT' [, 'START'|'END']) DuckDB: time_bucket(INTERVAL 'slice_length' UNIT, date_expr)

For 'END' kind, add the interval to get the end of the slice. For DATE type with 'END', cast result back to DATE to preserve type.

def bitmapbucketnumber_sql(self, expression: sqlglot.expressions.math.BitmapBucketNumber) -> str:
2352    def bitmapbucketnumber_sql(self, expression: exp.BitmapBucketNumber) -> str:
2353        """
2354        Transpile BITMAP_BUCKET_NUMBER function from Snowflake to DuckDB equivalent.
2355
2356        Snowflake's BITMAP_BUCKET_NUMBER returns a 1-based bucket identifier where:
2357        - Each bucket covers 32,768 values
2358        - Bucket numbering starts at 1
2359        - Formula: ((value - 1) // 32768) + 1 for positive values
2360
2361        For non-positive values (0 and negative), we use value // 32768 to avoid
2362        producing bucket 0 or positive bucket IDs for negative inputs.
2363        """
2364        value = expression.this
2365
2366        positive_formula = ((value - 1) // 32768) + 1
2367        non_positive_formula = value // 32768
2368
2369        # CASE WHEN value > 0 THEN ((value - 1) // 32768) + 1 ELSE value // 32768 END
2370        case_expr = (
2371            exp.case()
2372            .when(exp.GT(this=value, expression=exp.Literal.number(0)), positive_formula)
2373            .else_(non_positive_formula)
2374        )
2375        return self.sql(case_expr)

Transpile BITMAP_BUCKET_NUMBER function from Snowflake to DuckDB equivalent.

Snowflake's BITMAP_BUCKET_NUMBER returns a 1-based bucket identifier where:

  • Each bucket covers 32,768 values
  • Bucket numbering starts at 1
  • Formula: ((value - 1) // 32768) + 1 for positive values

For non-positive values (0 and negative), we use value // 32768 to avoid producing bucket 0 or positive bucket IDs for negative inputs.

def bitmapbitposition_sql(self, expression: sqlglot.expressions.math.BitmapBitPosition) -> str:
2377    def bitmapbitposition_sql(self, expression: exp.BitmapBitPosition) -> str:
2378        """
2379        Transpile Snowflake's BITMAP_BIT_POSITION to DuckDB CASE expression.
2380
2381        Snowflake's BITMAP_BIT_POSITION behavior:
2382        - For n <= 0: returns ABS(n) % 32768
2383        - For n > 0: returns (n - 1) % 32768 (maximum return value is 32767)
2384        """
2385        this = expression.this
2386
2387        return self.sql(
2388            exp.Mod(
2389                this=exp.Paren(
2390                    this=exp.If(
2391                        this=exp.GT(this=this, expression=exp.Literal.number(0)),
2392                        true=this - exp.Literal.number(1),
2393                        false=exp.Abs(this=this),
2394                    )
2395                ),
2396                expression=MAX_BIT_POSITION,
2397            )
2398        )

Transpile Snowflake's BITMAP_BIT_POSITION to DuckDB CASE expression.

Snowflake's BITMAP_BIT_POSITION behavior:

  • For n <= 0: returns ABS(n) % 32768
  • For n > 0: returns (n - 1) % 32768 (maximum return value is 32767)
def bitmapconstructagg_sql(self, expression: sqlglot.expressions.math.BitmapConstructAgg) -> str:
2400    def bitmapconstructagg_sql(self, expression: exp.BitmapConstructAgg) -> str:
2401        """
2402        Transpile Snowflake's BITMAP_CONSTRUCT_AGG to DuckDB equivalent.
2403        Uses a pre-parsed template with placeholders replaced by expression nodes.
2404
2405        Snowflake bitmap format:
2406        - Small (< 5 unique values): 2-byte count (big-endian) + values (little-endian) + padding to 10 bytes
2407        - Large (>= 5 unique values): 10-byte header (0x08 + 9 zeros) + values (little-endian)
2408        """
2409        arg = expression.this
2410        return (
2411            f"({self.sql(exp.replace_placeholders(self.BITMAP_CONSTRUCT_AGG_TEMPLATE, arg=arg))})"
2412        )

Transpile Snowflake's BITMAP_CONSTRUCT_AGG to DuckDB equivalent. Uses a pre-parsed template with placeholders replaced by expression nodes.

Snowflake bitmap format:

  • Small (< 5 unique values): 2-byte count (big-endian) + values (little-endian) + padding to 10 bytes
  • Large (>= 5 unique values): 10-byte header (0x08 + 9 zeros) + values (little-endian)
def getignorecase_sql(self, expression: sqlglot.expressions.functions.GetIgnoreCase) -> str:
2414    def getignorecase_sql(self, expression: exp.GetIgnoreCase) -> str:
2415        self.unsupported("DuckDB does not support the GET_IGNORE_CASE() function")
2416        return self.function_fallback_sql(expression)
def compress_sql(self, expression: sqlglot.expressions.string.Compress) -> str:
2418    def compress_sql(self, expression: exp.Compress) -> str:
2419        self.unsupported("DuckDB does not support the COMPRESS() function")
2420        return self.function_fallback_sql(expression)
def encrypt_sql(self, expression: sqlglot.expressions.string.Encrypt) -> str:
2422    def encrypt_sql(self, expression: exp.Encrypt) -> str:
2423        self.unsupported("ENCRYPT is not supported in DuckDB")
2424        return self.function_fallback_sql(expression)
def decrypt_sql(self, expression: sqlglot.expressions.string.Decrypt) -> str:
2426    def decrypt_sql(self, expression: exp.Decrypt) -> str:
2427        func_name = "TRY_DECRYPT" if expression.args.get("safe") else "DECRYPT"
2428        self.unsupported(f"{func_name} is not supported in DuckDB")
2429        return self.function_fallback_sql(expression)
def decryptraw_sql(self, expression: sqlglot.expressions.string.DecryptRaw) -> str:
2431    def decryptraw_sql(self, expression: exp.DecryptRaw) -> str:
2432        func_name = "TRY_DECRYPT_RAW" if expression.args.get("safe") else "DECRYPT_RAW"
2433        self.unsupported(f"{func_name} is not supported in DuckDB")
2434        return self.function_fallback_sql(expression)
def encryptraw_sql(self, expression: sqlglot.expressions.string.EncryptRaw) -> str:
2436    def encryptraw_sql(self, expression: exp.EncryptRaw) -> str:
2437        self.unsupported("ENCRYPT_RAW is not supported in DuckDB")
2438        return self.function_fallback_sql(expression)
def parseurl_sql(self, expression: sqlglot.expressions.string.ParseUrl) -> str:
2440    def parseurl_sql(self, expression: exp.ParseUrl) -> str:
2441        self.unsupported("PARSE_URL is not supported in DuckDB")
2442        return self.function_fallback_sql(expression)
def parseip_sql(self, expression: sqlglot.expressions.functions.ParseIp) -> str:
2444    def parseip_sql(self, expression: exp.ParseIp) -> str:
2445        self.unsupported("PARSE_IP is not supported in DuckDB")
2446        return self.function_fallback_sql(expression)
def decompressstring_sql(self, expression: sqlglot.expressions.string.DecompressString) -> str:
2448    def decompressstring_sql(self, expression: exp.DecompressString) -> str:
2449        self.unsupported("DECOMPRESS_STRING is not supported in DuckDB")
2450        return self.function_fallback_sql(expression)
def decompressbinary_sql(self, expression: sqlglot.expressions.string.DecompressBinary) -> str:
2452    def decompressbinary_sql(self, expression: exp.DecompressBinary) -> str:
2453        self.unsupported("DECOMPRESS_BINARY is not supported in DuckDB")
2454        return self.function_fallback_sql(expression)
def jarowinklersimilarity_sql(self, expression: sqlglot.expressions.math.JarowinklerSimilarity) -> str:
2456    def jarowinklersimilarity_sql(self, expression: exp.JarowinklerSimilarity) -> str:
2457        this = expression.this
2458        expr = expression.expression
2459
2460        if expression.args.get("case_insensitive"):
2461            this = exp.Upper(this=this)
2462            expr = exp.Upper(this=expr)
2463
2464        result = exp.func("JARO_WINKLER_SIMILARITY", this, expr)
2465
2466        if expression.args.get("integer_scale"):
2467            result = exp.cast(result * 100, "INTEGER")
2468
2469        return self.sql(result)
def nthvalue_sql(self, expression: sqlglot.expressions.aggregate.NthValue) -> str:
2471    def nthvalue_sql(self, expression: exp.NthValue) -> str:
2472        from_first = expression.args.get("from_first", True)
2473        if not from_first:
2474            self.unsupported("DuckDB's NTH_VALUE doesn't support starting from the end ")
2475
2476        return self.function_fallback_sql(expression)
def randstr_sql(self, expression: sqlglot.expressions.functions.Randstr) -> str:
2478    def randstr_sql(self, expression: exp.Randstr) -> str:
2479        """
2480        Transpile Snowflake's RANDSTR to DuckDB equivalent using deterministic hash-based random.
2481        Uses a pre-parsed template with placeholders replaced by expression nodes.
2482
2483        RANDSTR(length, generator) generates a random string of specified length.
2484        - With numeric seed: Use HASH(i + seed) for deterministic output (same seed = same result)
2485        - With RANDOM(): Use RANDOM() in the hash for non-deterministic output
2486        - No generator: Use default seed value
2487        """
2488        length = expression.this
2489        generator = expression.args.get("generator")
2490
2491        if generator:
2492            if isinstance(generator, exp.Rand):
2493                # If it's RANDOM(), use its seed if available, otherwise use RANDOM() itself
2494                seed_value = generator.this or generator
2495            else:
2496                # Const/int or other expression - use as seed directly
2497                seed_value = generator
2498        else:
2499            # No generator specified, use default seed (arbitrary but deterministic)
2500            seed_value = exp.Literal.number(RANDSTR_SEED)
2501
2502        replacements = {"seed": seed_value, "length": length}
2503        return f"({self.sql(exp.replace_placeholders(self.RANDSTR_TEMPLATE, **replacements))})"

Transpile Snowflake's RANDSTR to DuckDB equivalent using deterministic hash-based random. Uses a pre-parsed template with placeholders replaced by expression nodes.

RANDSTR(length, generator) generates a random string of specified length.

  • With numeric seed: Use HASH(i + seed) for deterministic output (same seed = same result)
  • With RANDOM(): Use RANDOM() in the hash for non-deterministic output
  • No generator: Use default seed value
@unsupported_args('finish')
def reduce_sql(self, expression: sqlglot.expressions.array.Reduce) -> str:
2505    @unsupported_args("finish")
2506    def reduce_sql(self, expression: exp.Reduce) -> str:
2507        array_arg = expression.this
2508        initial_value = expression.args.get("initial")
2509        merge_lambda = expression.args.get("merge")
2510
2511        if merge_lambda:
2512            merge_lambda.set("colon", True)
2513
2514        return self.func("list_reduce", array_arg, merge_lambda, initial_value)
def zipf_sql(self, expression: sqlglot.expressions.functions.Zipf) -> str:
2516    def zipf_sql(self, expression: exp.Zipf) -> str:
2517        """
2518        Transpile Snowflake's ZIPF to DuckDB using CDF-based inverse sampling.
2519        Uses a pre-parsed template with placeholders replaced by expression nodes.
2520        """
2521        s = expression.this
2522        n = expression.args["elementcount"]
2523        gen = expression.args["gen"]
2524
2525        if not isinstance(gen, exp.Rand):
2526            # (ABS(HASH(seed)) % 1000000) / 1000000.0
2527            random_expr: exp.Expr = exp.Div(
2528                this=exp.Paren(
2529                    this=exp.Mod(
2530                        this=exp.Abs(this=exp.Anonymous(this="HASH", expressions=[gen.copy()])),
2531                        expression=exp.Literal.number(1000000),
2532                    )
2533                ),
2534                expression=exp.Literal.number(1000000.0),
2535            )
2536        else:
2537            # Use RANDOM() for non-deterministic output
2538            random_expr = exp.Rand()
2539
2540        replacements = {"s": s, "n": n, "random_expr": random_expr}
2541        return f"({self.sql(exp.replace_placeholders(self.ZIPF_TEMPLATE, **replacements))})"

Transpile Snowflake's ZIPF to DuckDB using CDF-based inverse sampling. Uses a pre-parsed template with placeholders replaced by expression nodes.

def tobinary_sql(self, expression: sqlglot.expressions.string.ToBinary) -> str:
2543    def tobinary_sql(self, expression: exp.ToBinary) -> str:
2544        """
2545        TO_BINARY and TRY_TO_BINARY transpilation:
2546        - 'HEX': TO_BINARY('48454C50', 'HEX') -> UNHEX('48454C50')
2547        - 'UTF-8': TO_BINARY('TEST', 'UTF-8') -> ENCODE('TEST')
2548        - 'BASE64': TO_BINARY('SEVMUA==', 'BASE64') -> FROM_BASE64('SEVMUA==')
2549
2550        For TRY_TO_BINARY (safe=True), wrap with TRY():
2551        - 'HEX': TRY_TO_BINARY('invalid', 'HEX') -> TRY(UNHEX('invalid'))
2552        """
2553        value = expression.this
2554        format_arg = expression.args.get("format")
2555        is_safe = expression.args.get("safe")
2556        is_binary = _is_binary(expression)
2557
2558        if not format_arg and not is_binary:
2559            func_name = "TRY_TO_BINARY" if is_safe else "TO_BINARY"
2560            return self.func(func_name, value)
2561
2562        # Snowflake defaults to HEX encoding when no format is specified
2563        fmt = format_arg.name.upper() if format_arg else "HEX"
2564
2565        if fmt in ("UTF-8", "UTF8"):
2566            # DuckDB ENCODE always uses UTF-8, no charset parameter needed
2567            result = self.func("ENCODE", value)
2568        elif fmt == "BASE64":
2569            result = self.func("FROM_BASE64", value)
2570        elif fmt == "HEX":
2571            result = self.func("UNHEX", value)
2572        else:
2573            if is_safe:
2574                return self.sql(exp.null())
2575            else:
2576                self.unsupported(f"format {fmt} is not supported")
2577                result = self.func("TO_BINARY", value)
2578        return f"TRY({result})" if is_safe else result

TO_BINARY and TRY_TO_BINARY transpilation:

  • 'HEX': TO_BINARY('48454C50', 'HEX') -> UNHEX('48454C50')
  • 'UTF-8': TO_BINARY('TEST', 'UTF-8') -> ENCODE('TEST')
  • 'BASE64': TO_BINARY('SEVMUA==', 'BASE64') -> FROM_BASE64('SEVMUA==')

For TRY_TO_BINARY (safe=True), wrap with TRY():

  • 'HEX': TRY_TO_BINARY('invalid', 'HEX') -> TRY(UNHEX('invalid'))
def tonumber_sql(self, expression: sqlglot.expressions.string.ToNumber) -> str:
2580    def tonumber_sql(self, expression: exp.ToNumber) -> str:
2581        fmt = expression.args.get("format")
2582        precision = expression.args.get("precision")
2583        scale = expression.args.get("scale")
2584
2585        if not fmt and precision and scale:
2586            return self.sql(
2587                exp.cast(
2588                    expression.this, f"DECIMAL({precision.name}, {scale.name})", dialect="duckdb"
2589                )
2590            )
2591
2592        return super().tonumber_sql(expression)
def generator_sql(self, expression: sqlglot.expressions.array.Generator) -> str:
2618    def generator_sql(self, expression: exp.Generator) -> str:
2619        # Transpile Snowflake GENERATOR to DuckDB range()
2620        rowcount = expression.args.get("rowcount")
2621        time_limit = expression.args.get("time_limit")
2622
2623        if time_limit:
2624            self.unsupported("GENERATOR TIMELIMIT parameter is not supported in DuckDB")
2625
2626        if not rowcount:
2627            self.unsupported("GENERATOR without ROWCOUNT is not supported in DuckDB")
2628            return self.func("range", exp.Literal.number(0))
2629
2630        return self.func("range", rowcount)
def greatest_sql(self, expression: sqlglot.expressions.functions.Greatest) -> str:
2632    def greatest_sql(self, expression: exp.Greatest) -> str:
2633        return self._greatest_least_sql(expression)
def least_sql(self, expression: sqlglot.expressions.functions.Least) -> str:
2635    def least_sql(self, expression: exp.Least) -> str:
2636        return self._greatest_least_sql(expression)
def lambda_sql( self, expression: sqlglot.expressions.query.Lambda, arrow_sep: str = '->', wrap: bool = True) -> str:
2638    def lambda_sql(self, expression: exp.Lambda, arrow_sep: str = "->", wrap: bool = True) -> str:
2639        if expression.args.get("colon"):
2640            prefix = "LAMBDA "
2641            arrow_sep = ":"
2642            wrap = False
2643        else:
2644            prefix = ""
2645
2646        lambda_sql = super().lambda_sql(expression, arrow_sep=arrow_sep, wrap=wrap)
2647        return f"{prefix}{lambda_sql}"
def show_sql(self, expression: sqlglot.expressions.ddl.Show) -> str:
2649    def show_sql(self, expression: exp.Show) -> str:
2650        from_ = self.sql(expression, "from_")
2651        from_ = f" FROM {from_}" if from_ else ""
2652        return f"SHOW {expression.name}{from_}"
def soundex_sql(self, expression: sqlglot.expressions.string.Soundex) -> str:
2654    def soundex_sql(self, expression: exp.Soundex) -> str:
2655        self.unsupported("SOUNDEX is not supported in DuckDB")
2656        return self.func("SOUNDEX", expression.this)
def sortarray_sql(self, expression: sqlglot.expressions.array.SortArray) -> str:
2658    def sortarray_sql(self, expression: exp.SortArray) -> str:
2659        arr = expression.this
2660        asc = expression.args.get("asc")
2661        nulls_first = expression.args.get("nulls_first")
2662
2663        if not isinstance(asc, exp.Boolean) and not isinstance(nulls_first, exp.Boolean):
2664            return self.func("LIST_SORT", arr, asc, nulls_first)
2665
2666        nulls_are_first = nulls_first == exp.true()
2667        nulls_first_sql = exp.Literal.string("NULLS FIRST") if nulls_are_first else None
2668
2669        if not isinstance(asc, exp.Boolean):
2670            return self.func("LIST_SORT", arr, asc, nulls_first_sql)
2671
2672        descending = asc == exp.false()
2673
2674        if not descending and not nulls_are_first:
2675            return self.func("LIST_SORT", arr)
2676        if not nulls_are_first:
2677            return self.func("ARRAY_REVERSE_SORT", arr)
2678        return self.func(
2679            "LIST_SORT",
2680            arr,
2681            exp.Literal.string("DESC" if descending else "ASC"),
2682            exp.Literal.string("NULLS FIRST"),
2683        )
def install_sql(self, expression: sqlglot.expressions.ddl.Install) -> str:
2685    def install_sql(self, expression: exp.Install) -> str:
2686        force = "FORCE " if expression.args.get("force") else ""
2687        this = self.sql(expression, "this")
2688        from_clause = expression.args.get("from_")
2689        from_clause = f" FROM {from_clause}" if from_clause else ""
2690        return f"{force}INSTALL {this}{from_clause}"
def approxtopk_sql(self, expression: sqlglot.expressions.aggregate.ApproxTopK) -> str:
2692    def approxtopk_sql(self, expression: exp.ApproxTopK) -> str:
2693        self.unsupported(
2694            "APPROX_TOP_K cannot be transpiled to DuckDB due to incompatible return types. "
2695        )
2696        return self.function_fallback_sql(expression)
def strposition_sql(self, expression: sqlglot.expressions.string.StrPosition) -> str:
2698    def strposition_sql(self, expression: exp.StrPosition) -> str:
2699        this = expression.this
2700        substr = expression.args.get("substr")
2701        position = expression.args.get("position")
2702
2703        # For BINARY/BLOB: DuckDB's STRPOS doesn't support BLOB types
2704        # Convert to HEX strings, use STRPOS, then convert hex position to byte position
2705        if _is_binary(this):
2706            # Build expression: STRPOS(HEX(haystack), HEX(needle))
2707            hex_strpos = exp.StrPosition(
2708                this=exp.Hex(this=this),
2709                substr=exp.Hex(this=substr),
2710            )
2711
2712            return self.sql(exp.cast((hex_strpos + 1) / 2, exp.DType.INT))
2713
2714        # For VARCHAR: handle clamp_position
2715        if expression.args.get("clamp_position") and position:
2716            expression = expression.copy()
2717            expression.set(
2718                "position",
2719                exp.If(
2720                    this=exp.LTE(this=position, expression=exp.Literal.number(0)),
2721                    true=exp.Literal.number(1),
2722                    false=position.copy(),
2723                ),
2724            )
2725
2726        return strposition_sql(self, expression)
def substring_sql(self, expression: sqlglot.expressions.string.Substring) -> str:
2728    def substring_sql(self, expression: exp.Substring) -> str:
2729        if expression.args.get("zero_start"):
2730            start = expression.args.get("start")
2731            length = expression.args.get("length")
2732
2733            if start := expression.args.get("start"):
2734                start = exp.If(this=start.eq(0), true=exp.Literal.number(1), false=start)
2735            if length := expression.args.get("length"):
2736                length = exp.If(this=length < 0, true=exp.Literal.number(0), false=length)
2737
2738            return self.func("SUBSTRING", expression.this, start, length)
2739
2740        return self.function_fallback_sql(expression)
def strtotime_sql(self, expression: sqlglot.expressions.temporal.StrToTime) -> str:
2742    def strtotime_sql(self, expression: exp.StrToTime) -> str:
2743        # Check if target_type requires TIMESTAMPTZ (for LTZ/TZ variants)
2744        target_type = expression.args.get("target_type")
2745        needs_tz = target_type and target_type.this in (
2746            exp.DType.TIMESTAMPLTZ,
2747            exp.DType.TIMESTAMPTZ,
2748        )
2749
2750        value, formatted_time = self._strptime_default_year(expression)
2751
2752        if expression.args.get("safe"):
2753            cast_type = exp.DType.TIMESTAMPTZ if needs_tz else exp.DType.TIMESTAMP
2754            return self.sql(exp.cast(self.func("TRY_STRPTIME", value, formatted_time), cast_type))
2755
2756        base_sql = self.func("STRPTIME", value, formatted_time)
2757        if needs_tz:
2758            return self.sql(
2759                exp.cast(
2760                    base_sql,
2761                    exp.DataType(this=exp.DType.TIMESTAMPTZ),
2762                )
2763            )
2764        return base_sql
def strtodate_sql(self, expression: sqlglot.expressions.temporal.StrToDate) -> str:
2766    def strtodate_sql(self, expression: exp.StrToDate) -> str:
2767        value, formatted_time = self._strptime_default_year(expression)
2768        function_name = "STRPTIME" if not expression.args.get("safe") else "TRY_STRPTIME"
2769        return self.sql(
2770            exp.cast(
2771                self.func(function_name, value, formatted_time),
2772                exp.DataType(this=exp.DType.DATE),
2773            )
2774        )
def parsedatetime_sql(self, expression: sqlglot.expressions.temporal.ParseDatetime) -> str:
2788    def parsedatetime_sql(self, expression: exp.ParseDatetime) -> str:
2789        value, formatted_time = self._strptime_default_year(expression)
2790        return self.func("STRPTIME", value, formatted_time)
def parsetime_sql(self, expression: sqlglot.expressions.temporal.ParseTime) -> str:
2792    def parsetime_sql(self, expression: exp.ParseTime) -> str:
2793        formatted_time = self.format_time(expression)
2794        return self.sql(
2795            exp.cast(
2796                self.func("STRPTIME", expression.this, formatted_time),
2797                exp.DataType(this=exp.DType.TIME),
2798            )
2799        )
def tsordstotime_sql(self, expression: sqlglot.expressions.temporal.TsOrDsToTime) -> str:
2801    def tsordstotime_sql(self, expression: exp.TsOrDsToTime) -> str:
2802        this = expression.this
2803        time_format = self.format_time(expression)
2804        safe = expression.args.get("safe")
2805        time_type = exp.DataType.from_str("TIME", dialect="duckdb")
2806        cast_expr = exp.TryCast if safe else exp.Cast
2807
2808        if time_format:
2809            func_name = "TRY_STRPTIME" if safe else "STRPTIME"
2810            strptime = exp.Anonymous(this=func_name, expressions=[this, time_format])
2811            return self.sql(cast_expr(this=strptime, to=time_type))
2812
2813        if isinstance(this, exp.TsOrDsToTime) or this.is_type(exp.DType.TIME):
2814            return self.sql(this)
2815
2816        return self.sql(cast_expr(this=this, to=time_type))
def currentdate_sql(self, expression: sqlglot.expressions.temporal.CurrentDate) -> str:
2818    def currentdate_sql(self, expression: exp.CurrentDate) -> str:
2819        if not expression.this:
2820            return "CURRENT_DATE"
2821
2822        expr = exp.Cast(
2823            this=exp.AtTimeZone(this=exp.CurrentTimestamp(), zone=expression.this),
2824            to=exp.DataType(this=exp.DType.DATE),
2825        )
2826        return self.sql(expr)
def checkjson_sql(self, expression: sqlglot.expressions.json.CheckJson) -> str:
2828    def checkjson_sql(self, expression: exp.CheckJson) -> str:
2829        arg = expression.this
2830        return self.sql(
2831            exp.case()
2832            .when(
2833                exp.or_(arg.is_(exp.Null()), arg.eq(""), exp.func("json_valid", arg)),
2834                exp.null(),
2835            )
2836            .else_(exp.Literal.string("Invalid JSON"))
2837        )
def parsejson_sql(self, expression: sqlglot.expressions.json.ParseJSON) -> str:
2839    def parsejson_sql(self, expression: exp.ParseJSON) -> str:
2840        arg = expression.this
2841        if expression.args.get("safe"):
2842            return self.sql(
2843                exp.case()
2844                .when(exp.func("json_valid", arg), exp.cast(arg.copy(), "JSON"))
2845                .else_(exp.null())
2846            )
2847        return self.func("JSON", arg)
def unicode_sql(self, expression: sqlglot.expressions.string.Unicode) -> str:
2849    def unicode_sql(self, expression: exp.Unicode) -> str:
2850        if expression.args.get("empty_is_zero"):
2851            return self.sql(
2852                exp.case()
2853                .when(expression.this.eq(exp.Literal.string("")), exp.Literal.number(0))
2854                .else_(exp.Anonymous(this="UNICODE", expressions=[expression.this]))
2855            )
2856
2857        return self.func("UNICODE", expression.this)
def stripnullvalue_sql(self, expression: sqlglot.expressions.json.StripNullValue) -> str:
2859    def stripnullvalue_sql(self, expression: exp.StripNullValue) -> str:
2860        return self.sql(
2861            exp.case()
2862            .when(exp.func("json_type", expression.this).eq("NULL"), exp.null())
2863            .else_(expression.this)
2864        )
def trunc_sql(self, expression: sqlglot.expressions.math.Trunc) -> str:
2866    def trunc_sql(self, expression: exp.Trunc) -> str:
2867        decimals = expression.args.get("decimals")
2868        if (
2869            expression.args.get("fractions_supported")
2870            and decimals
2871            and not decimals.is_type(exp.DType.INT)
2872        ):
2873            decimals = exp.cast(decimals, exp.DType.INT, dialect="duckdb")
2874
2875        return self.func("TRUNC", expression.this, decimals)
def normal_sql(self, expression: sqlglot.expressions.functions.Normal) -> str:
2877    def normal_sql(self, expression: exp.Normal) -> str:
2878        """
2879        Transpile Snowflake's NORMAL(mean, stddev, gen) to DuckDB.
2880
2881        Uses the Box-Muller transform via NORMAL_TEMPLATE.
2882        """
2883        mean = expression.this
2884        stddev = expression.args["stddev"]
2885        gen: exp.Expr = expression.args["gen"]
2886
2887        # Build two uniform random values [0, 1) for Box-Muller transform
2888        if isinstance(gen, exp.Rand) and gen.this is None:
2889            u1: exp.Expr = exp.Rand()
2890            u2: exp.Expr = exp.Rand()
2891        else:
2892            # Seeded: derive two values using HASH with different inputs
2893            seed = gen.this if isinstance(gen, exp.Rand) else gen
2894            u1 = exp.replace_placeholders(self.SEEDED_RANDOM_TEMPLATE, seed=seed)
2895            u2 = exp.replace_placeholders(
2896                self.SEEDED_RANDOM_TEMPLATE,
2897                seed=exp.Add(this=seed.copy(), expression=exp.Literal.number(1)),
2898            )
2899
2900        replacements = {"mean": mean, "stddev": stddev, "u1": u1, "u2": u2}
2901        return self.sql(exp.replace_placeholders(self.NORMAL_TEMPLATE, **replacements))

Transpile Snowflake's NORMAL(mean, stddev, gen) to DuckDB.

Uses the Box-Muller transform via NORMAL_TEMPLATE.

def uniform_sql(self, expression: sqlglot.expressions.functions.Uniform) -> str:
2903    def uniform_sql(self, expression: exp.Uniform) -> str:
2904        """
2905        Transpile Snowflake's UNIFORM(min, max, gen) to DuckDB.
2906
2907        UNIFORM returns a random value in [min, max]:
2908        - Integer result if both min and max are integers
2909        - Float result if either min or max is a float
2910        """
2911        min_val = expression.this
2912        max_val = expression.expression
2913        gen = expression.args.get("gen")
2914
2915        # Determine if result should be integer (both bounds are integers).
2916        # We do this to emulate Snowflake's behavior, INT -> INT, FLOAT -> FLOAT
2917        is_int_result = min_val.is_int and max_val.is_int
2918
2919        # Build the random value expression [0, 1)
2920        if not isinstance(gen, exp.Rand):
2921            # Seed value: (ABS(HASH(seed)) % 1000000) / 1000000.0
2922            random_expr: exp.Expr = exp.Div(
2923                this=exp.Paren(
2924                    this=exp.Mod(
2925                        this=exp.Abs(this=exp.Anonymous(this="HASH", expressions=[gen])),
2926                        expression=exp.Literal.number(1000000),
2927                    )
2928                ),
2929                expression=exp.Literal.number(1000000.0),
2930            )
2931        else:
2932            random_expr = exp.Rand()
2933
2934        # Build: min + random * (max - min [+ 1 for int])
2935        range_expr: exp.Expr = exp.Sub(this=max_val, expression=min_val)
2936        if is_int_result:
2937            range_expr = exp.Add(this=range_expr, expression=exp.Literal.number(1))
2938
2939        result: exp.Expr = exp.Add(
2940            this=min_val,
2941            expression=exp.Mul(this=random_expr, expression=exp.Paren(this=range_expr)),
2942        )
2943
2944        if is_int_result:
2945            result = exp.Cast(this=exp.Floor(this=result), to=exp.DType.BIGINT.into_expr())
2946
2947        return self.sql(result)

Transpile Snowflake's UNIFORM(min, max, gen) to DuckDB.

UNIFORM returns a random value in [min, max]:

  • Integer result if both min and max are integers
  • Float result if either min or max is a float
def timefromparts_sql(self, expression: sqlglot.expressions.temporal.TimeFromParts) -> str:
2949    def timefromparts_sql(self, expression: exp.TimeFromParts) -> str:
2950        nano = expression.args.get("nano")
2951        overflow = expression.args.get("overflow")
2952
2953        # Snowflake's TIME_FROM_PARTS supports overflow
2954        if overflow:
2955            hour = expression.args["hour"]
2956            minute = expression.args["min"]
2957            sec = expression.args["sec"]
2958
2959            # Check if values are within normal ranges - use MAKE_TIME for efficiency
2960            if not nano and all(arg.is_int for arg in [hour, minute, sec]):
2961                try:
2962                    h_val = hour.to_py()
2963                    m_val = minute.to_py()
2964                    s_val = sec.to_py()
2965                    if 0 <= h_val <= 23 and 0 <= m_val <= 59 and 0 <= s_val <= 59:
2966                        return rename_func("MAKE_TIME")(self, expression)
2967                except ValueError:
2968                    pass
2969
2970            # Overflow or nanoseconds detected - use INTERVAL arithmetic
2971            if nano:
2972                sec = sec + nano.pop() / exp.Literal.number(1000000000.0)
2973
2974            total_seconds = hour * exp.Literal.number(3600) + minute * exp.Literal.number(60) + sec
2975
2976            return self.sql(
2977                exp.Add(
2978                    this=exp.Cast(
2979                        this=exp.Literal.string("00:00:00"), to=exp.DType.TIME.into_expr()
2980                    ),
2981                    expression=exp.Interval(this=total_seconds, unit=exp.var("SECOND")),
2982                )
2983            )
2984
2985        # Default: MAKE_TIME
2986        if nano:
2987            expression.set(
2988                "sec", expression.args["sec"] + nano.pop() / exp.Literal.number(1000000000.0)
2989            )
2990
2991        return rename_func("MAKE_TIME")(self, expression)
def extract_sql(self, expression: sqlglot.expressions.temporal.Extract) -> str:
2993    def extract_sql(self, expression: exp.Extract) -> str:
2994        """
2995        Transpile EXTRACT/DATE_PART for DuckDB, handling specifiers not natively supported.
2996
2997        DuckDB doesn't support: WEEKISO, YEAROFWEEK, YEAROFWEEKISO, NANOSECOND,
2998        EPOCH_SECOND (as integer), EPOCH_MILLISECOND, EPOCH_MICROSECOND, EPOCH_NANOSECOND
2999        """
3000        this = expression.this
3001        datetime_expr = expression.expression
3002
3003        # TIMESTAMPTZ extractions may produce different results between Snowflake and DuckDB
3004        # because Snowflake applies server timezone while DuckDB uses local timezone
3005        if datetime_expr.is_type(exp.DType.TIMESTAMPTZ, exp.DType.TIMESTAMPLTZ):
3006            self.unsupported(
3007                "EXTRACT from TIMESTAMPTZ / TIMESTAMPLTZ may produce different results due to timezone handling differences"
3008            )
3009
3010        part_name = this.name.upper()
3011
3012        if part_name in self.EXTRACT_STRFTIME_MAPPINGS:
3013            fmt, cast_type = self.EXTRACT_STRFTIME_MAPPINGS[part_name]
3014
3015            # Problem: strftime doesn't accept TIME and there's no NANOSECOND function
3016            # So, for NANOSECOND with TIME, fallback to MICROSECOND * 1000
3017            is_nano_time = part_name == "NANOSECOND" and datetime_expr.is_type(
3018                exp.DType.TIME, exp.DType.TIMETZ
3019            )
3020
3021            if is_nano_time:
3022                self.unsupported("Parameter NANOSECOND is not supported with TIME type in DuckDB")
3023                return self.sql(
3024                    exp.cast(
3025                        exp.Mul(
3026                            this=exp.Extract(this=exp.var("MICROSECOND"), expression=datetime_expr),
3027                            expression=exp.Literal.number(1000),
3028                        ),
3029                        exp.DataType.from_str(cast_type, dialect="duckdb"),
3030                    )
3031                )
3032
3033            # For NANOSECOND, cast to TIMESTAMP_NS to preserve nanosecond precision
3034            strftime_input = datetime_expr
3035            if part_name == "NANOSECOND":
3036                strftime_input = exp.cast(datetime_expr, exp.DType.TIMESTAMP_NS)
3037
3038            return self.sql(
3039                exp.cast(
3040                    exp.Anonymous(
3041                        this="STRFTIME",
3042                        expressions=[strftime_input, exp.Literal.string(fmt)],
3043                    ),
3044                    exp.DataType.from_str(cast_type, dialect="duckdb"),
3045                )
3046            )
3047
3048        if part_name in self.EXTRACT_EPOCH_MAPPINGS:
3049            func_name = self.EXTRACT_EPOCH_MAPPINGS[part_name]
3050            result: exp.Expr = exp.Anonymous(this=func_name, expressions=[datetime_expr])
3051            # EPOCH returns float, cast to BIGINT for integer result
3052            if part_name == "EPOCH_SECOND":
3053                result = exp.cast(result, exp.DataType.from_str("BIGINT", dialect="duckdb"))
3054            return self.sql(result)
3055
3056        return super().extract_sql(expression)

Transpile EXTRACT/DATE_PART for DuckDB, handling specifiers not natively supported.

DuckDB doesn't support: WEEKISO, YEAROFWEEK, YEAROFWEEKISO, NANOSECOND, EPOCH_SECOND (as integer), EPOCH_MILLISECOND, EPOCH_MICROSECOND, EPOCH_NANOSECOND

def timestampfromparts_sql(self, expression: sqlglot.expressions.temporal.TimestampFromParts) -> str:
3058    def timestampfromparts_sql(self, expression: exp.TimestampFromParts) -> str:
3059        # Check if this is the date/time expression form: TIMESTAMP_FROM_PARTS(date_expr, time_expr)
3060        date_expr = expression.this
3061        time_expr = expression.expression
3062
3063        if date_expr is not None and time_expr is not None:
3064            # In DuckDB, DATE + TIME produces TIMESTAMP
3065            return self.sql(exp.Add(this=date_expr, expression=time_expr))
3066
3067        # Component-based form: TIMESTAMP_FROM_PARTS(year, month, day, hour, minute, second, ...)
3068        sec = expression.args.get("sec")
3069        if sec is None:
3070            # This shouldn't happen with valid input, but handle gracefully
3071            return rename_func("MAKE_TIMESTAMP")(self, expression)
3072
3073        milli = expression.args.get("milli")
3074        if milli is not None:
3075            sec += milli.pop() / exp.Literal.number(1000.0)
3076
3077        nano = expression.args.get("nano")
3078        if nano is not None:
3079            sec += nano.pop() / exp.Literal.number(1000000000.0)
3080
3081        if milli or nano:
3082            expression.set("sec", sec)
3083
3084        return rename_func("MAKE_TIMESTAMP")(self, expression)
@unsupported_args('nano')
def timestampltzfromparts_sql( self, expression: sqlglot.expressions.temporal.TimestampLtzFromParts) -> str:
3086    @unsupported_args("nano")
3087    def timestampltzfromparts_sql(self, expression: exp.TimestampLtzFromParts) -> str:
3088        # Pop nano so rename_func only passes args that MAKE_TIMESTAMP accepts
3089        if nano := expression.args.get("nano"):
3090            nano.pop()
3091
3092        timestamp = rename_func("MAKE_TIMESTAMP")(self, expression)
3093        return f"CAST({timestamp} AS TIMESTAMPTZ)"
@unsupported_args('nano')
def timestamptzfromparts_sql( self, expression: sqlglot.expressions.temporal.TimestampTzFromParts) -> str:
3095    @unsupported_args("nano")
3096    def timestamptzfromparts_sql(self, expression: exp.TimestampTzFromParts) -> str:
3097        # Extract zone before popping
3098        zone = expression.args.get("zone")
3099        # Pop zone and nano so rename_func only passes args that MAKE_TIMESTAMP accepts
3100        if zone:
3101            zone = zone.pop()
3102
3103        if nano := expression.args.get("nano"):
3104            nano.pop()
3105
3106        timestamp = rename_func("MAKE_TIMESTAMP")(self, expression)
3107
3108        if zone:
3109            # Use AT TIME ZONE to apply the explicit timezone
3110            return f"{timestamp} AT TIME ZONE {self.sql(zone)}"
3111
3112        return timestamp
def tablesample_sql( self, expression: sqlglot.expressions.query.TableSample, tablesample_keyword: str | None = None) -> str:
3114    def tablesample_sql(
3115        self,
3116        expression: exp.TableSample,
3117        tablesample_keyword: str | None = None,
3118    ) -> str:
3119        if not isinstance(expression.parent, exp.Select):
3120            # This sample clause only applies to a single source, not the entire resulting relation
3121            tablesample_keyword = "TABLESAMPLE"
3122
3123        if expression.args.get("size"):
3124            method = expression.args.get("method")
3125            if method and method.name.upper() != "RESERVOIR":
3126                self.unsupported(
3127                    f"Sampling method {method} is not supported with a discrete sample count, "
3128                    "defaulting to reservoir sampling"
3129                )
3130                expression.set("method", exp.var("RESERVOIR"))
3131
3132        return super().tablesample_sql(expression, tablesample_keyword=tablesample_keyword)
def in_sql(self, expression: sqlglot.expressions.core.In) -> str:
3134    def in_sql(self, expression: exp.In) -> str:
3135        unnest = expression.args.get("unnest")
3136        if unnest:
3137            return self.sql(
3138                exp.replace_placeholders(
3139                    self.IN_UNNEST_TEMPLATE, arr=unnest.expressions[0], value=expression.this
3140                )
3141            )
3142        return super().in_sql(expression)
def join_sql(self, expression: sqlglot.expressions.query.Join) -> str:
3144    def join_sql(self, expression: exp.Join) -> str:
3145        if (
3146            not expression.args.get("using")
3147            and not expression.args.get("on")
3148            and not expression.method
3149            and (expression.kind in ("", "INNER", "OUTER"))
3150        ):
3151            # Some dialects support `LEFT/INNER JOIN UNNEST(...)` without an explicit ON clause
3152            # DuckDB doesn't, but we can just add a dummy ON clause that is always true
3153            if isinstance(expression.this, exp.Unnest):
3154                return super().join_sql(expression.on(exp.true()))
3155
3156            expression.set("side", None)
3157            expression.set("kind", None)
3158
3159        return super().join_sql(expression)
def countif_sql(self, expression: sqlglot.expressions.aggregate.CountIf) -> str:
3161    def countif_sql(self, expression: exp.CountIf) -> str:
3162        if self.dialect.version >= (1, 2):
3163            this = expression.this
3164            if expression.args.get("zero_on_all_null") and not isinstance(this, exp.Distinct):
3165                # DuckDB >= 1.2's COUNT_IF returns NULL when the condition is NULL on all rows,
3166                # so we wrap the condition in IS TRUE to preserve count-like semantics
3167                expression = exp.CountIf(this=exp.paren(this).is_(exp.true()))
3168            return self.function_fallback_sql(expression)
3169
3170        # https://github.com/tobymao/sqlglot/pull/4749
3171        return count_if_to_sum(self, expression)
def bracket_sql(self, expression: sqlglot.expressions.core.Bracket) -> str:
3173    def bracket_sql(self, expression: exp.Bracket) -> str:
3174        if self.dialect.version >= (1, 2):
3175            return super().bracket_sql(expression)
3176
3177        # https://duckdb.org/2025/02/05/announcing-duckdb-120.html#breaking-changes
3178        this = expression.this
3179        if isinstance(this, exp.Array):
3180            this.replace(exp.paren(this))
3181
3182        bracket = super().bracket_sql(expression)
3183
3184        if not expression.args.get("returns_list_for_maps"):
3185            if not this.type:
3186                from sqlglot.optimizer.annotate_types import annotate_types
3187
3188                this = annotate_types(this, dialect=self.dialect)
3189
3190            if this.is_type(exp.DType.MAP):
3191                bracket = f"({bracket})[1]"
3192
3193        return bracket
def withingroup_sql(self, expression: sqlglot.expressions.core.WithinGroup) -> str:
3195    def withingroup_sql(self, expression: exp.WithinGroup) -> str:
3196        func = expression.this
3197
3198        # For ARRAY_AGG, DuckDB requires ORDER BY inside the function, not in WITHIN GROUP
3199        # Transform: ARRAY_AGG(x) WITHIN GROUP (ORDER BY y) -> ARRAY_AGG(x ORDER BY y)
3200        if isinstance(func, exp.ArrayAgg):
3201            if not isinstance(order := expression.expression, exp.Order):
3202                return self.sql(func)
3203
3204            # Save the original column for FILTER clause (before wrapping with Order)
3205            original_this = func.this
3206
3207            # Move ORDER BY inside ARRAY_AGG by wrapping its argument with Order
3208            # ArrayAgg.this should become Order(this=ArrayAgg.this, expressions=order.expressions)
3209            func.set(
3210                "this",
3211                exp.Order(
3212                    this=func.this.copy(),
3213                    expressions=order.expressions,
3214                ),
3215            )
3216
3217            # Generate the ARRAY_AGG function with ORDER BY and add FILTER clause if needed
3218            # Use original_this (not the Order-wrapped version) for the FILTER condition
3219            array_agg_sql = self.function_fallback_sql(func)
3220            return self._add_arrayagg_null_filter(array_agg_sql, func, original_this)
3221
3222        # For other functions (like PERCENTILES), use existing logic
3223        expression_sql = self.sql(expression, "expression")
3224
3225        if isinstance(func, exp.PERCENTILES):
3226            # Make the order key the first arg and slide the fraction to the right
3227            # https://duckdb.org/docs/sql/aggregates#ordered-set-aggregate-functions
3228            order_col = expression.find(exp.Ordered)
3229            if order_col:
3230                func.set("expression", func.this)
3231                func.set("this", order_col.this)
3232
3233        this = self.sql(expression, "this").rstrip(")")
3234
3235        return f"{this}{expression_sql})"
def length_sql(self, expression: sqlglot.expressions.string.Length) -> str:
3237    def length_sql(self, expression: exp.Length) -> str:
3238        arg = expression.this
3239
3240        # Dialects like BQ and Snowflake also accept binary values as args, so
3241        # DDB will attempt to infer the type or resort to case/when resolution
3242        if not expression.args.get("binary") or arg.is_string:
3243            return self.func("LENGTH", arg)
3244
3245        if not arg.type:
3246            from sqlglot.optimizer.annotate_types import annotate_types
3247
3248            arg = annotate_types(arg, dialect=self.dialect)
3249
3250        if arg.is_type(*exp.DataType.TEXT_TYPES):
3251            return self.func("LENGTH", arg)
3252
3253        # We need these casts to make duckdb's static type checker happy
3254        blob = exp.cast(arg, exp.DType.VARBINARY)
3255        varchar = exp.cast(arg, exp.DType.VARCHAR)
3256
3257        case = (
3258            exp.case(exp.Anonymous(this="TYPEOF", expressions=[arg]))
3259            .when(exp.Literal.string("BLOB"), exp.ByteLength(this=blob))
3260            .else_(exp.Anonymous(this="LENGTH", expressions=[varchar]))
3261        )
3262        return self.sql(case)
def bitlength_sql(self, expression: sqlglot.expressions.string.BitLength) -> str:
3264    def bitlength_sql(self, expression: exp.BitLength) -> str:
3265        if not _is_binary(arg := expression.this):
3266            return self.func("BIT_LENGTH", arg)
3267
3268        blob = exp.cast(arg, exp.DataType.Type.VARBINARY)
3269        return self.sql(exp.ByteLength(this=blob) * exp.Literal.number(8))
def chr_sql( self, expression: sqlglot.expressions.string.Chr, name: str = 'CHR') -> str:
3271    def chr_sql(self, expression: exp.Chr, name: str = "CHR") -> str:
3272        arg = expression.expressions[0]
3273        if arg.is_type(*exp.DataType.REAL_TYPES):
3274            arg = exp.cast(arg, exp.DType.INT)
3275        return self.func("CHR", arg)
def collation_sql(self, expression: sqlglot.expressions.functions.Collation) -> str:
3277    def collation_sql(self, expression: exp.Collation) -> str:
3278        self.unsupported("COLLATION function is not supported by DuckDB")
3279        return self.function_fallback_sql(expression)
def collate_sql(self, expression: sqlglot.expressions.functions.Collate) -> str:
3281    def collate_sql(self, expression: exp.Collate) -> str:
3282        if not expression.expression.is_string:
3283            return super().collate_sql(expression)
3284
3285        raw = expression.expression.name
3286        if not raw:
3287            return self.sql(expression.this)
3288
3289        parts = []
3290        for part in raw.split("-"):
3291            lower = part.lower()
3292            if lower not in _SNOWFLAKE_COLLATION_DEFAULTS:
3293                if lower in _SNOWFLAKE_COLLATION_UNSUPPORTED:
3294                    self.unsupported(
3295                        f"Snowflake collation specifier '{part}' has no DuckDB equivalent"
3296                    )
3297                parts.append(lower)
3298
3299        if not parts:
3300            return self.sql(expression.this)
3301        return super().collate_sql(
3302            exp.Collate(this=expression.this, expression=exp.var(".".join(parts)))
3303        )
def regexpcount_sql(self, expression: sqlglot.expressions.string.RegexpCount) -> str:
3335    def regexpcount_sql(self, expression: exp.RegexpCount) -> str:
3336        this = expression.this
3337        pattern = expression.expression
3338        position = expression.args.get("position")
3339        parameters = expression.args.get("parameters")
3340
3341        # Validate flags - only "ims" flags are supported for embedded patterns
3342        validated_flags = self._validate_regexp_flags(parameters, supported_flags="ims")
3343
3344        if position:
3345            this = exp.Substring(this=this, start=position)
3346
3347        # Embed flags in pattern (REGEXP_EXTRACT_ALL doesn't support flags argument)
3348        if validated_flags:
3349            pattern = exp.Concat(expressions=[exp.Literal.string(f"(?{validated_flags})"), pattern])
3350
3351        # Handle empty pattern: Snowflake returns 0, DuckDB would match between every character
3352        result = (
3353            exp.case()
3354            .when(
3355                exp.EQ(this=pattern, expression=exp.Literal.string("")),
3356                exp.Literal.number(0),
3357            )
3358            .else_(
3359                exp.Length(
3360                    this=exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern])
3361                )
3362            )
3363        )
3364
3365        return self.sql(result)
def regexpreplace_sql(self, expression: sqlglot.expressions.string.RegexpReplace) -> str:
3367    def regexpreplace_sql(self, expression: exp.RegexpReplace) -> str:
3368        subject = expression.this
3369        pattern = expression.expression
3370        replacement = expression.args.get("replacement") or exp.Literal.string("")
3371        position = expression.args.get("position")
3372        occurrence = expression.args.get("occurrence")
3373        modifiers = expression.args.get("modifiers")
3374
3375        validated_flags = self._validate_regexp_flags(modifiers, supported_flags="cimsg") or ""
3376
3377        # Handle occurrence (only literals supported)
3378        if occurrence and not occurrence.is_int:
3379            self.unsupported("REGEXP_REPLACE with non-literal occurrence")
3380        else:
3381            occurrence = occurrence.to_py() if occurrence and occurrence.is_int else 0
3382            if occurrence > 1:
3383                self.unsupported(f"REGEXP_REPLACE occurrence={occurrence} not supported")
3384            # flag duckdb to do either all or none, single_replace check is for duckdb round trip
3385            elif (
3386                occurrence == 0
3387                and "g" not in validated_flags
3388                and not expression.args.get("single_replace")
3389            ):
3390                validated_flags += "g"
3391
3392        # Handle position (only literals supported)
3393        prefix = None
3394        if position and not position.is_int:
3395            self.unsupported("REGEXP_REPLACE with non-literal position")
3396        elif position and position.is_int and position.to_py() > 1:
3397            pos = position.to_py()
3398            prefix = exp.Substring(
3399                this=subject, start=exp.Literal.number(1), length=exp.Literal.number(pos - 1)
3400            )
3401            subject = exp.Substring(this=subject, start=exp.Literal.number(pos))
3402
3403        result: exp.Expr = exp.Anonymous(
3404            this="REGEXP_REPLACE",
3405            expressions=[
3406                subject,
3407                pattern,
3408                replacement,
3409                exp.Literal.string(validated_flags) if validated_flags else None,
3410            ],
3411        )
3412
3413        if prefix:
3414            result = exp.Concat(expressions=[prefix, result])
3415
3416        return self.sql(result)
def regexplike_sql(self, expression: sqlglot.expressions.core.RegexpLike) -> str:
3418    def regexplike_sql(self, expression: exp.RegexpLike) -> str:
3419        this = expression.this
3420        pattern = expression.expression
3421        flag = expression.args.get("flag")
3422
3423        if expression.args.get("full_match"):
3424            validated_flags = self._validate_regexp_flags(flag, supported_flags="cims")
3425            flag = exp.Literal.string(validated_flags) if validated_flags else None
3426            return self.func("REGEXP_FULL_MATCH", this, pattern, flag)
3427
3428        return self.func("REGEXP_MATCHES", this, pattern, flag)
@unsupported_args('ins_cost', 'del_cost', 'sub_cost')
def levenshtein_sql(self, expression: sqlglot.expressions.string.Levenshtein) -> str:
3430    @unsupported_args("ins_cost", "del_cost", "sub_cost")
3431    def levenshtein_sql(self, expression: exp.Levenshtein) -> str:
3432        this = expression.this
3433        expr = expression.expression
3434        max_dist = expression.args.get("max_dist")
3435
3436        if max_dist is None:
3437            return self.func("LEVENSHTEIN", this, expr)
3438
3439        # Emulate Snowflake semantics: if distance > max_dist, return max_dist
3440        levenshtein = exp.Levenshtein(this=this, expression=expr)
3441        return self.sql(exp.Least(this=levenshtein, expressions=[max_dist]))
def pad_sql(self, expression: sqlglot.expressions.string.Pad) -> str:
3443    def pad_sql(self, expression: exp.Pad) -> str:
3444        """
3445        Handle RPAD/LPAD for VARCHAR and BINARY types.
3446
3447        For VARCHAR: Delegate to parent class
3448        For BINARY: Lower to: input || REPEAT(pad, GREATEST(0, target_len - OCTET_LENGTH(input)))
3449        """
3450        string_arg = expression.this
3451        fill_arg = expression.args.get("fill_pattern") or exp.Literal.string(" ")
3452
3453        if _is_binary(string_arg) or _is_binary(fill_arg):
3454            length_arg = expression.expression
3455            is_left = expression.args.get("is_left")
3456
3457            input_len = exp.ByteLength(this=string_arg)
3458            chars_needed = length_arg - input_len
3459            pad_count = exp.Greatest(
3460                this=exp.Literal.number(0), expressions=[chars_needed], ignore_nulls=True
3461            )
3462            repeat_expr = exp.Repeat(this=fill_arg, times=pad_count)
3463
3464            left, right = string_arg, repeat_expr
3465            if is_left:
3466                left, right = right, left
3467
3468            result = exp.DPipe(this=left, expression=right)
3469            return self.sql(result)
3470
3471        # For VARCHAR: Delegate to parent class (handles PAD_FILL_PATTERN_IS_REQUIRED)
3472        return super().pad_sql(expression)

Handle RPAD/LPAD for VARCHAR and BINARY types.

For VARCHAR: Delegate to parent class For BINARY: Lower to: input || REPEAT(pad, GREATEST(0, target_len - OCTET_LENGTH(input)))

def minhash_sql(self, expression: sqlglot.expressions.aggregate.Minhash) -> str:
3474    def minhash_sql(self, expression: exp.Minhash) -> str:
3475        k = expression.this
3476        exprs = expression.expressions
3477
3478        if len(exprs) != 1 or isinstance(exprs[0], exp.Star):
3479            self.unsupported(
3480                "MINHASH with multiple expressions or * requires manual query restructuring"
3481            )
3482            return self.func("MINHASH", k, *exprs)
3483
3484        expr = exprs[0]
3485        result = exp.replace_placeholders(self.MINHASH_TEMPLATE.copy(), expr=expr, k=k)
3486        return f"({self.sql(result)})"
def minhashcombine_sql(self, expression: sqlglot.expressions.aggregate.MinhashCombine) -> str:
3488    def minhashcombine_sql(self, expression: exp.MinhashCombine) -> str:
3489        expr = expression.this
3490        result = exp.replace_placeholders(self.MINHASH_COMBINE_TEMPLATE.copy(), expr=expr)
3491        return f"({self.sql(result)})"
def approximatesimilarity_sql( self, expression: sqlglot.expressions.aggregate.ApproximateSimilarity) -> str:
3493    def approximatesimilarity_sql(self, expression: exp.ApproximateSimilarity) -> str:
3494        expr = expression.this
3495        result = exp.replace_placeholders(self.APPROXIMATE_SIMILARITY_TEMPLATE.copy(), expr=expr)
3496        return f"({self.sql(result)})"
def arrayuniqueagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayUniqueAgg) -> str:
3498    def arrayuniqueagg_sql(self, expression: exp.ArrayUniqueAgg) -> str:
3499        return self.sql(
3500            exp.Filter(
3501                this=exp.func("LIST", exp.Distinct(expressions=[expression.this])),
3502                expression=exp.Where(this=expression.this.copy().is_(exp.null()).not_()),
3503            )
3504        )
def arrayconcatagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayConcatAgg) -> str:
3506    def arrayconcatagg_sql(self, expression: exp.ArrayConcatAgg) -> str:
3507        this = expression.this
3508
3509        if isinstance(this, exp.Limit):
3510            self.unsupported("LIMIT in ARRAY_CONCAT_AGG cannot be transpiled to DuckDB")
3511            this = this.this
3512
3513        inner = this.this if isinstance(this, exp.Order) else this
3514
3515        return self.func(
3516            "FLATTEN",
3517            exp.Filter(
3518                this=exp.ArrayAgg(this=this),
3519                expression=exp.Where(this=inner.copy().is_(exp.null()).not_()),
3520            ),
3521        )
def arrayunionagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayUnionAgg) -> str:
3523    def arrayunionagg_sql(self, expression: exp.ArrayUnionAgg) -> str:
3524        self.unsupported("ARRAY_UNION_AGG is not supported in DuckDB")
3525        return self.function_fallback_sql(expression)
def arraydistinct_sql(self, expression: sqlglot.expressions.array.ArrayDistinct) -> str:
3527    def arraydistinct_sql(self, expression: exp.ArrayDistinct) -> str:
3528        arr = expression.this
3529        func = self.func("LIST_DISTINCT", arr)
3530
3531        if expression.args.get("check_null"):
3532            add_null_to_array = exp.func(
3533                "LIST_APPEND", exp.func("LIST_DISTINCT", exp.ArrayCompact(this=arr)), exp.Null()
3534            )
3535            return self.sql(
3536                exp.If(
3537                    this=exp.NEQ(
3538                        this=exp.ArraySize(this=arr), expression=exp.func("LIST_COUNT", arr)
3539                    ),
3540                    true=add_null_to_array,
3541                    false=func,
3542                )
3543            )
3544
3545        return func
def arrayintersect_sql(self, expression: sqlglot.expressions.array.ArrayIntersect) -> str:
3547    def arrayintersect_sql(self, expression: exp.ArrayIntersect) -> str:
3548        if expression.args.get("is_multiset") and len(expression.expressions) == 2:
3549            return self._array_bag_sql(
3550                self.ARRAY_INTERSECTION_CONDITION,
3551                expression.expressions[0],
3552                expression.expressions[1],
3553            )
3554        return self.function_fallback_sql(expression)
def arrayexcept_sql(self, expression: sqlglot.expressions.array.ArrayExcept) -> str:
3556    def arrayexcept_sql(self, expression: exp.ArrayExcept) -> str:
3557        arr1, arr2 = expression.this, expression.expression
3558        if expression.args.get("is_multiset"):
3559            return self._array_bag_sql(self.ARRAY_EXCEPT_CONDITION, arr1, arr2)
3560        return self.sql(
3561            exp.replace_placeholders(self.ARRAY_EXCEPT_SET_TEMPLATE, arr1=arr1, arr2=arr2)
3562        )
def arrayslice_sql(self, expression: sqlglot.expressions.array.ArraySlice) -> str:
3564    def arrayslice_sql(self, expression: exp.ArraySlice) -> str:
3565        """
3566        Transpiles Snowflake's ARRAY_SLICE (0-indexed, exclusive end) to DuckDB's
3567        ARRAY_SLICE (1-indexed, inclusive end) by wrapping start and end in CASE
3568        expressions that adjust the index at query time:
3569          - start: CASE WHEN start >= 0 THEN start + 1 ELSE start END
3570          - end:   CASE WHEN end < 0 THEN end - 1 ELSE end END
3571        """
3572        start, end = expression.args.get("start"), expression.args.get("end")
3573
3574        if expression.args.get("zero_based"):
3575            if start is not None:
3576                start = (
3577                    exp.case()
3578                    .when(
3579                        exp.GTE(this=start.copy(), expression=exp.Literal.number(0)),
3580                        exp.Add(this=start.copy(), expression=exp.Literal.number(1)),
3581                    )
3582                    .else_(start)
3583                )
3584            if end is not None:
3585                end = (
3586                    exp.case()
3587                    .when(
3588                        exp.LT(this=end.copy(), expression=exp.Literal.number(0)),
3589                        exp.Sub(this=end.copy(), expression=exp.Literal.number(1)),
3590                    )
3591                    .else_(end)
3592                )
3593
3594        return self.func("ARRAY_SLICE", expression.this, start, end, expression.args.get("step"))

Transpiles Snowflake's ARRAY_SLICE (0-indexed, exclusive end) to DuckDB's ARRAY_SLICE (1-indexed, inclusive end) by wrapping start and end in CASE expressions that adjust the index at query time:

  • start: CASE WHEN start >= 0 THEN start + 1 ELSE start END
  • end: CASE WHEN end < 0 THEN end - 1 ELSE end END
def arrayszip_sql(self, expression: sqlglot.expressions.array.ArraysZip) -> str:
3596    def arrayszip_sql(self, expression: exp.ArraysZip) -> str:
3597        args = expression.expressions
3598
3599        if not args:
3600            # Return [{}] - using MAP([], []) since DuckDB can't represent empty structs
3601            return self.sql(exp.array(exp.Map(keys=exp.array(), values=exp.array())))
3602
3603        # Build placeholder values for template
3604        lengths = [exp.Length(this=arg) for arg in args]
3605        max_len = (
3606            lengths[0]
3607            if len(lengths) == 1
3608            else exp.Greatest(this=lengths[0], expressions=lengths[1:])
3609        )
3610
3611        # Empty struct with same schema: {'$1': NULL, '$2': NULL, ...}
3612        empty_struct = exp.func(
3613            "STRUCT",
3614            *[
3615                exp.PropertyEQ(this=exp.Literal.string(f"${i + 1}"), expression=exp.Null())
3616                for i in range(len(args))
3617            ],
3618        )
3619
3620        # Struct for transform: {'$1': COALESCE(arr1, [])[__i + 1], ...}
3621        # COALESCE wrapping handles NULL arrays - prevents invalid NULL[i] syntax
3622        index = exp.column("__i") + 1
3623        transform_struct = exp.func(
3624            "STRUCT",
3625            *[
3626                exp.PropertyEQ(
3627                    this=exp.Literal.string(f"${i + 1}"),
3628                    expression=exp.func("COALESCE", arg, exp.array())[index],
3629                )
3630                for i, arg in enumerate(args)
3631            ],
3632        )
3633
3634        result = exp.replace_placeholders(
3635            self.ARRAYS_ZIP_TEMPLATE.copy(),
3636            null_check=exp.or_(*[arg.is_(exp.Null()) for arg in args]),
3637            all_empty_check=exp.and_(
3638                *[
3639                    exp.EQ(this=exp.Length(this=arg), expression=exp.Literal.number(0))
3640                    for arg in args
3641                ]
3642            ),
3643            empty_struct=empty_struct,
3644            max_len=max_len,
3645            transform_struct=transform_struct,
3646        )
3647        return self.sql(result)
def lower_sql(self, expression: sqlglot.expressions.string.Lower) -> str:
3649    def lower_sql(self, expression: exp.Lower) -> str:
3650        result_sql = self.func("LOWER", _cast_to_varchar(expression.this))
3651        return _gen_with_cast_to_blob(self, expression, result_sql)
def upper_sql(self, expression: sqlglot.expressions.string.Upper) -> str:
3653    def upper_sql(self, expression: exp.Upper) -> str:
3654        result_sql = self.func("UPPER", _cast_to_varchar(expression.this))
3655        return _gen_with_cast_to_blob(self, expression, result_sql)
def reverse_sql(self, expression: sqlglot.expressions.string.Reverse) -> str:
3657    def reverse_sql(self, expression: exp.Reverse) -> str:
3658        result_sql = self.func("REVERSE", _cast_to_varchar(expression.this))
3659        return _gen_with_cast_to_blob(self, expression, result_sql)
def left_sql(self, expression: sqlglot.expressions.string.Left) -> str:
3685    def left_sql(self, expression: exp.Left) -> str:
3686        return self._left_right_sql(expression, "LEFT")
def right_sql(self, expression: sqlglot.expressions.string.Right) -> str:
3688    def right_sql(self, expression: exp.Right) -> str:
3689        return self._left_right_sql(expression, "RIGHT")
def rtrimmedlength_sql(self, expression: sqlglot.expressions.string.RtrimmedLength) -> str:
3691    def rtrimmedlength_sql(self, expression: exp.RtrimmedLength) -> str:
3692        return self.func("LENGTH", exp.Trim(this=expression.this, position="TRAILING"))
def stuff_sql(self, expression: sqlglot.expressions.string.Stuff) -> str:
3694    def stuff_sql(self, expression: exp.Stuff) -> str:
3695        base = expression.this
3696        start = expression.args["start"]
3697        length = expression.args["length"]
3698        insertion = expression.expression
3699        is_binary = _is_binary(base)
3700
3701        if is_binary:
3702            # DuckDB's SUBSTRING doesn't accept BLOB; operate on the HEX string instead
3703            # (each byte = 2 hex chars), then UNHEX back to BLOB
3704            base = exp.Hex(this=base)
3705            insertion = exp.Hex(this=insertion)
3706            left = exp.Substring(
3707                this=base.copy(),
3708                start=exp.Literal.number(1),
3709                length=(start.copy() - exp.Literal.number(1)) * exp.Literal.number(2),
3710            )
3711            right = exp.Substring(
3712                this=base.copy(),
3713                start=((start + length) - exp.Literal.number(1)) * exp.Literal.number(2)
3714                + exp.Literal.number(1),
3715            )
3716        else:
3717            left = exp.Substring(
3718                this=base.copy(),
3719                start=exp.Literal.number(1),
3720                length=start.copy() - exp.Literal.number(1),
3721            )
3722            right = exp.Substring(this=base.copy(), start=start + length)
3723        result: exp.Expr = exp.DPipe(
3724            this=exp.DPipe(this=left, expression=insertion), expression=right
3725        )
3726
3727        if is_binary:
3728            result = exp.Unhex(this=result)
3729
3730        return self.sql(result)
def rand_sql(self, expression: sqlglot.expressions.functions.Rand) -> str:
3732    def rand_sql(self, expression: exp.Rand) -> str:
3733        seed = expression.this
3734        if seed is not None:
3735            self.unsupported("RANDOM with seed is not supported in DuckDB")
3736
3737        lower = expression.args.get("lower")
3738        upper = expression.args.get("upper")
3739
3740        if lower and upper:
3741            # scale DuckDB's [0,1) to the specified range
3742            range_size = exp.paren(upper - lower)
3743            scaled = exp.Add(this=lower, expression=exp.func("random") * range_size)
3744
3745            # For now we assume that if bounds are set, return type is BIGINT. Snowflake/Teradata
3746            result = exp.cast(scaled, exp.DType.BIGINT)
3747            return self.sql(result)
3748
3749        # Default DuckDB behavior - just return RANDOM() as float
3750        return "RANDOM()"
def bytelength_sql(self, expression: sqlglot.expressions.string.ByteLength) -> str:
3752    def bytelength_sql(self, expression: exp.ByteLength) -> str:
3753        arg = expression.this
3754
3755        # Check if it's a text type (handles both literals and annotated expressions)
3756        if arg.is_type(*exp.DataType.TEXT_TYPES):
3757            return self.func("OCTET_LENGTH", exp.Encode(this=arg))
3758
3759        # Default: pass through as-is (conservative for DuckDB, handles binary and unannotated)
3760        return self.func("OCTET_LENGTH", arg)
def base64encode_sql(self, expression: sqlglot.expressions.string.Base64Encode) -> str:
3762    def base64encode_sql(self, expression: exp.Base64Encode) -> str:
3763        # DuckDB TO_BASE64 requires BLOB input
3764        # Snowflake BASE64_ENCODE accepts both VARCHAR and BINARY - for VARCHAR it implicitly
3765        # encodes UTF-8 bytes. We add ENCODE unless the input is a binary type.
3766        result = expression.this
3767
3768        # Check if input is a string type - ENCODE only accepts VARCHAR
3769        if result.is_type(*exp.DataType.TEXT_TYPES):
3770            result = exp.Encode(this=result)
3771
3772        result = exp.ToBase64(this=result)
3773
3774        max_line_length = expression.args.get("max_line_length")
3775        alphabet = expression.args.get("alphabet")
3776
3777        # Handle custom alphabet by replacing standard chars with custom ones
3778        result = _apply_base64_alphabet_replacements(result, alphabet)
3779
3780        # Handle max_line_length by inserting newlines every N characters
3781        line_length = (
3782            t.cast(int, max_line_length.to_py())
3783            if isinstance(max_line_length, exp.Literal) and max_line_length.is_number
3784            else 0
3785        )
3786        if line_length > 0:
3787            newline = exp.Chr(expressions=[exp.Literal.number(10)])
3788            result = exp.Trim(
3789                this=exp.RegexpReplace(
3790                    this=result,
3791                    expression=exp.Literal.string(f"(.{{{line_length}}})"),
3792                    replacement=exp.Concat(expressions=[exp.Literal.string("\\1"), newline.copy()]),
3793                ),
3794                expression=newline,
3795                position="TRAILING",
3796            )
3797
3798        return self.sql(result)
def hex_sql(self, expression: sqlglot.expressions.string.Hex) -> str:
3800    def hex_sql(self, expression: exp.Hex) -> str:
3801        case = expression.args.get("case")
3802
3803        if not case:
3804            return self.func("HEX", expression.this)
3805
3806        hex_expr = exp.Hex(this=expression.this)
3807        return self.sql(
3808            exp.case()
3809            .when(case.is_(exp.null()), exp.null())
3810            .when(case.copy().eq(0), exp.Lower(this=hex_expr.copy()))
3811            .else_(hex_expr)
3812        )
def replace_sql(self, expression: sqlglot.expressions.string.Replace) -> str:
3814    def replace_sql(self, expression: exp.Replace) -> str:
3815        result_sql = self.func(
3816            "REPLACE",
3817            _cast_to_varchar(expression.this),
3818            _cast_to_varchar(expression.expression),
3819            _cast_to_varchar(expression.args.get("replacement")),
3820        )
3821        return _gen_with_cast_to_blob(self, expression, result_sql)
def bitwisexor_sql(self, expression: sqlglot.expressions.core.BitwiseXor) -> str:
3828    def bitwisexor_sql(self, expression: exp.BitwiseXor) -> str:
3829        _prepare_binary_bitwise_args(expression)
3830        result_sql = self.func("XOR", expression.this, expression.expression)
3831        return _gen_with_cast_to_blob(self, expression, result_sql)
def objectinsert_sql(self, expression: sqlglot.expressions.json.ObjectInsert) -> str:
3833    def objectinsert_sql(self, expression: exp.ObjectInsert) -> str:
3834        this = expression.this
3835        key = expression.args.get("key")
3836        key_sql = key.name if isinstance(key, exp.Expr) else ""
3837        value_sql = self.sql(expression, "value")
3838
3839        kv_sql = f"{key_sql} := {value_sql}"
3840
3841        # If the input struct is empty e.g. transpiling OBJECT_INSERT(OBJECT_CONSTRUCT(), key, value) from Snowflake
3842        # then we can generate STRUCT_PACK which will build it since STRUCT_INSERT({}, key := value) is not valid DuckDB
3843        if isinstance(this, exp.Struct) and not this.expressions:
3844            return self.func("STRUCT_PACK", kv_sql)
3845
3846        return self.func("STRUCT_INSERT", this, kv_sql)
def mapcat_sql(self, expression: sqlglot.expressions.array.MapCat) -> str:
3848    def mapcat_sql(self, expression: exp.MapCat) -> str:
3849        result = exp.replace_placeholders(
3850            self.MAPCAT_TEMPLATE.copy(),
3851            map1=expression.this,
3852            map2=expression.expression,
3853        )
3854        return self.sql(result)
def mapcontainskey_sql(self, expression: sqlglot.expressions.array.MapContainsKey) -> str:
3856    def mapcontainskey_sql(self, expression: exp.MapContainsKey) -> str:
3857        return self.func(
3858            "ARRAY_CONTAINS", exp.func("MAP_KEYS", expression.args["key"]), expression.this
3859        )
def mapdelete_sql(self, expression: sqlglot.expressions.array.MapDelete) -> str:
3861    def mapdelete_sql(self, expression: exp.MapDelete) -> str:
3862        map_arg = expression.this
3863        keys_to_delete = expression.expressions
3864
3865        x_dot_key = exp.Dot(this=exp.to_identifier("x"), expression=exp.to_identifier("key"))
3866
3867        lambda_expr = exp.Lambda(
3868            this=exp.In(this=x_dot_key, expressions=keys_to_delete).not_(),
3869            expressions=[exp.to_identifier("x")],
3870        )
3871        result = exp.func(
3872            "MAP_FROM_ENTRIES",
3873            exp.ArrayFilter(this=exp.func("MAP_ENTRIES", map_arg), expression=lambda_expr),
3874        )
3875        return self.sql(result)
def mappick_sql(self, expression: sqlglot.expressions.array.MapPick) -> str:
3877    def mappick_sql(self, expression: exp.MapPick) -> str:
3878        map_arg = expression.this
3879        keys_to_pick = expression.expressions
3880
3881        x_dot_key = exp.Dot(this=exp.to_identifier("x"), expression=exp.to_identifier("key"))
3882
3883        if len(keys_to_pick) == 1 and keys_to_pick[0].is_type(exp.DType.ARRAY):
3884            lambda_expr = exp.Lambda(
3885                this=exp.func("ARRAY_CONTAINS", keys_to_pick[0], x_dot_key),
3886                expressions=[exp.to_identifier("x")],
3887            )
3888        else:
3889            lambda_expr = exp.Lambda(
3890                this=exp.In(this=x_dot_key, expressions=keys_to_pick),
3891                expressions=[exp.to_identifier("x")],
3892            )
3893
3894        result = exp.func(
3895            "MAP_FROM_ENTRIES",
3896            exp.func("LIST_FILTER", exp.func("MAP_ENTRIES", map_arg), lambda_expr),
3897        )
3898        return self.sql(result)
def mapsize_sql(self, expression: sqlglot.expressions.array.MapSize) -> str:
3900    def mapsize_sql(self, expression: exp.MapSize) -> str:
3901        return self.func("CARDINALITY", expression.this)
@unsupported_args('update_flag')
def mapinsert_sql(self, expression: sqlglot.expressions.array.MapInsert) -> str:
3903    @unsupported_args("update_flag")
3904    def mapinsert_sql(self, expression: exp.MapInsert) -> str:
3905        map_arg = expression.this
3906        key = expression.args.get("key")
3907        value = expression.args.get("value")
3908
3909        map_type = map_arg.type
3910
3911        if value is not None:
3912            if map_type and map_type.expressions and len(map_type.expressions) > 1:
3913                # Extract the value type from MAP(key_type, value_type)
3914                value_type = map_type.expressions[1]
3915                # Cast value to match the map's value type to avoid type conflicts
3916                value = exp.cast(value, value_type)
3917            # else: polymorphic MAP case - no type parameters available, use value as-is
3918
3919        # Create a single-entry map for the new key-value pair
3920        new_entry_struct = exp.Struct(expressions=[exp.PropertyEQ(this=key, expression=value)])
3921        new_entry: exp.Expression = exp.ToMap(this=new_entry_struct)
3922
3923        # Use MAP_CONCAT to merge the original map with the new entry
3924        # This automatically handles both insert and update cases
3925        result = exp.func("MAP_CONCAT", map_arg, new_entry)
3926
3927        return self.sql(result)
def startswith_sql(self, expression: sqlglot.expressions.string.StartsWith) -> str:
3929    def startswith_sql(self, expression: exp.StartsWith) -> str:
3930        return self.func(
3931            "STARTS_WITH",
3932            _cast_to_varchar(expression.this),
3933            _cast_to_varchar(expression.expression),
3934        )
def space_sql(self, expression: sqlglot.expressions.string.Space) -> str:
3936    def space_sql(self, expression: exp.Space) -> str:
3937        # DuckDB's REPEAT requires BIGINT for the count parameter
3938        return self.sql(
3939            exp.Repeat(
3940                this=exp.Literal.string(" "),
3941                times=exp.cast(expression.this, exp.DType.BIGINT),
3942            )
3943        )
def tablefromrows_sql(self, expression: sqlglot.expressions.query.TableFromRows) -> str:
3945    def tablefromrows_sql(self, expression: exp.TableFromRows) -> str:
3946        # For GENERATOR, unwrap TABLE() - just emit the Generator (becomes RANGE)
3947        if isinstance(expression.this, exp.Generator):
3948            # Preserve alias, joins, and other table-level args
3949            table = exp.Table(
3950                this=expression.this,
3951                alias=expression.args.get("alias"),
3952                joins=expression.args.get("joins"),
3953            )
3954            return self.sql(table)
3955
3956        return super().tablefromrows_sql(expression)
def unnest_sql(self, expression: sqlglot.expressions.array.Unnest) -> str:
3958    def unnest_sql(self, expression: exp.Unnest) -> str:
3959        explode_array = expression.args.get("explode_array")
3960        if explode_array:
3961            # In BigQuery, UNNESTing a nested array leads to explosion of the top-level array & struct
3962            # This is transpiled to DDB by transforming "FROM UNNEST(...)" to "FROM (SELECT UNNEST(..., max_depth => 2))"
3963            expression.expressions.append(
3964                exp.Kwarg(this=exp.var("max_depth"), expression=exp.Literal.number(2))
3965            )
3966
3967            # If BQ's UNNEST is aliased, we transform it from a column alias to a table alias in DDB
3968            alias = expression.args.get("alias")
3969            if isinstance(alias, exp.TableAlias):
3970                expression.set("alias", None)
3971                if alias.columns:
3972                    alias = exp.TableAlias(this=seq_get(alias.columns, 0))
3973
3974            unnest_sql = super().unnest_sql(expression)
3975            select = exp.Select(expressions=[unnest_sql]).subquery(alias)
3976            return self.sql(select)
3977
3978        return super().unnest_sql(expression)
def arrayagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayAgg) -> str:
3980    def arrayagg_sql(self, expression: exp.ArrayAgg) -> str:
3981        if isinstance(expression.this, exp.Limit):
3982            self.unsupported("LIMIT inside ARRAY_AGG is not supported in DuckDB")
3983
3984        return super().arrayagg_sql(expression)
def ignorenulls_sql(self, expression: sqlglot.expressions.core.IgnoreNulls) -> str:
3986    def ignorenulls_sql(self, expression: exp.IgnoreNulls) -> str:
3987        this = expression.this
3988
3989        if isinstance(this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
3990            # DuckDB should render IGNORE NULLS only for the general-purpose
3991            # window functions that accept it e.g. FIRST_VALUE(... IGNORE NULLS) OVER (...)
3992            return super().ignorenulls_sql(expression)
3993
3994        # For ARRAY_AGG(expr IGNORE NULLS ...), convert IGNORE NULLS to a
3995        # FILTER(WHERE expr IS NOT NULL) clause by setting nulls_excluded on
3996        # the ArrayAgg.  The existing _add_arrayagg_null_filter method will
3997        # emit the FILTER clause during arrayagg_sql / withingroup_sql.
3998        if isinstance(this, exp.ArrayAgg):
3999            this.set("nulls_excluded", True)
4000            return self.sql(this)
4001
4002        if isinstance(this, exp.First):
4003            this = exp.AnyValue(this=this.this)
4004
4005        if not isinstance(this, (exp.AnyValue, exp.ApproxQuantiles)):
4006            self.unsupported("IGNORE NULLS is not supported for non-window functions.")
4007
4008        return self.sql(this)
def split_sql(self, expression: sqlglot.expressions.string.Split) -> str:
4010    def split_sql(self, expression: exp.Split) -> str:
4011        base_func = exp.func("STR_SPLIT", expression.this, expression.expression)
4012
4013        case_expr = exp.case().else_(base_func)
4014        needs_case = False
4015
4016        if expression.args.get("null_returns_null"):
4017            case_expr = case_expr.when(expression.expression.is_(exp.null()), exp.null())
4018            needs_case = True
4019
4020        if expression.args.get("empty_delimiter_returns_whole"):
4021            # When delimiter is empty string, return input string as single array element
4022            array_with_input = exp.array(expression.this)
4023            case_expr = case_expr.when(
4024                expression.expression.eq(exp.Literal.string("")), array_with_input
4025            )
4026            needs_case = True
4027
4028        return self.sql(case_expr if needs_case else base_func)
def splitpart_sql(self, expression: sqlglot.expressions.string.SplitPart) -> str:
4030    def splitpart_sql(self, expression: exp.SplitPart) -> str:
4031        string_arg = expression.this
4032        delimiter_arg = expression.args.get("delimiter")
4033        part_index_arg = expression.args.get("part_index")
4034
4035        if delimiter_arg and part_index_arg:
4036            # Handle Snowflake's "index 0 and 1 both return first element" behavior
4037            if expression.args.get("part_index_zero_as_one"):
4038                # Convert 0 to 1 for compatibility
4039
4040                part_index_arg = exp.Paren(
4041                    this=exp.case()
4042                    .when(part_index_arg.eq(exp.Literal.number("0")), exp.Literal.number("1"))
4043                    .else_(part_index_arg)
4044                )
4045
4046            # Use Anonymous to avoid recursion
4047            base_func_expr: exp.Expr = exp.Anonymous(
4048                this="SPLIT_PART", expressions=[string_arg, delimiter_arg, part_index_arg]
4049            )
4050            needs_case_transform = False
4051            case_expr = exp.case().else_(base_func_expr)
4052
4053            if expression.args.get("empty_delimiter_returns_whole"):
4054                # When delimiter is empty string:
4055                # - Return whole string if part_index is 1 or -1
4056                # - Return empty string otherwise
4057                empty_case = exp.Paren(
4058                    this=exp.case()
4059                    .when(
4060                        exp.or_(
4061                            part_index_arg.eq(exp.Literal.number("1")),
4062                            part_index_arg.eq(exp.Literal.number("-1")),
4063                        ),
4064                        string_arg,
4065                    )
4066                    .else_(exp.Literal.string(""))
4067                )
4068
4069                case_expr = case_expr.when(delimiter_arg.eq(exp.Literal.string("")), empty_case)
4070                needs_case_transform = True
4071
4072            """
4073            Output looks something like this:
4074
4075            CASE
4076            WHEN delimiter is '' THEN
4077                (
4078                    CASE
4079                    WHEN adjusted_part_index = 1 OR adjusted_part_index = -1 THEN input
4080                    ELSE '' END
4081                )
4082            ELSE SPLIT_PART(input, delimiter, adjusted_part_index)
4083            END
4084
4085            """
4086            return self.sql(case_expr if needs_case_transform else base_func_expr)
4087
4088        return self.function_fallback_sql(expression)
def respectnulls_sql(self, expression: sqlglot.expressions.core.RespectNulls) -> str:
4090    def respectnulls_sql(self, expression: exp.RespectNulls) -> str:
4091        if isinstance(expression.this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
4092            # DuckDB should render RESPECT NULLS only for the general-purpose
4093            # window functions that accept it e.g. FIRST_VALUE(... RESPECT NULLS) OVER (...)
4094            return super().respectnulls_sql(expression)
4095
4096        self.unsupported("RESPECT NULLS is not supported for non-window functions.")
4097        return self.sql(expression, "this")
def arraytostring_sql(self, expression: sqlglot.expressions.array.ArrayToString) -> str:
4099    def arraytostring_sql(self, expression: exp.ArrayToString) -> str:
4100        null = expression.args.get("null")
4101
4102        if expression.args.get("null_is_empty"):
4103            x = exp.to_identifier("x")
4104            list_transform = exp.Transform(
4105                this=expression.this.copy(),
4106                expression=exp.Lambda(
4107                    this=exp.Coalesce(
4108                        this=exp.cast(x, "TEXT"), expressions=[exp.Literal.string("")]
4109                    ),
4110                    expressions=[x],
4111                ),
4112            )
4113            array_to_string = exp.ArrayToString(
4114                this=list_transform, expression=expression.expression
4115            )
4116            if expression.args.get("null_delim_is_null"):
4117                return self.sql(
4118                    exp.case()
4119                    .when(expression.expression.copy().is_(exp.null()), exp.null())
4120                    .else_(array_to_string)
4121                )
4122            return self.sql(array_to_string)
4123
4124        if null:
4125            x = exp.to_identifier("x")
4126            return self.sql(
4127                exp.ArrayToString(
4128                    this=exp.Transform(
4129                        this=expression.this,
4130                        expression=exp.Lambda(
4131                            this=exp.Coalesce(this=x, expressions=[null]),
4132                            expressions=[x],
4133                        ),
4134                    ),
4135                    expression=expression.expression,
4136                )
4137            )
4138
4139        return self.func("ARRAY_TO_STRING", expression.this, expression.expression)
def concatws_sql(self, expression: sqlglot.expressions.string.ConcatWs) -> str:
4141    def concatws_sql(self, expression: exp.ConcatWs) -> str:
4142        # DuckDB-specific: handle binary types using DPipe (||) operator
4143        separator = seq_get(expression.expressions, 0)
4144        args = expression.expressions[1:]
4145
4146        if any(_is_binary(arg) for arg in [separator, *args]):
4147            result = args[0]
4148            for arg in args[1:]:
4149                result = exp.DPipe(
4150                    this=exp.DPipe(this=result, expression=separator), expression=arg
4151                )
4152            return self.sql(result)
4153
4154        return super().concatws_sql(expression)
def regexpextract_sql(self, expression: sqlglot.expressions.string.RegexpExtract) -> str:
4209    def regexpextract_sql(self, expression: exp.RegexpExtract) -> str:
4210        return self._regexp_extract_sql(expression)
def regexpextractall_sql(self, expression: sqlglot.expressions.string.RegexpExtractAll) -> str:
4212    def regexpextractall_sql(self, expression: exp.RegexpExtractAll) -> str:
4213        return self._regexp_extract_sql(expression)
def regexpinstr_sql(self, expression: sqlglot.expressions.string.RegexpInstr) -> str:
4215    def regexpinstr_sql(self, expression: exp.RegexpInstr) -> str:
4216        this = expression.this
4217        pattern = expression.expression
4218        position = expression.args.get("position")
4219        orig_occ = expression.args.get("occurrence")
4220        occurrence = orig_occ or exp.Literal.number(1)
4221        option = expression.args.get("option")
4222        parameters = expression.args.get("parameters")
4223
4224        validated_flags = self._validate_regexp_flags(parameters, supported_flags="ims")
4225        if validated_flags:
4226            pattern = exp.Concat(expressions=[exp.Literal.string(f"(?{validated_flags})"), pattern])
4227
4228        # Handle starting position offset
4229        pos_offset: exp.Expr = exp.Literal.number(0)
4230        if position and (not position.is_int or position.to_py() > 1):
4231            this = exp.Substring(this=this, start=position)
4232            pos_offset = position - exp.Literal.number(1)
4233
4234        # Helper: LIST_SUM(LIST_TRANSFORM(list[1:end], x -> LENGTH(x)))
4235        def sum_lengths(func_name: str, end: exp.Expr) -> exp.Expr:
4236            lst = exp.Bracket(
4237                this=exp.Anonymous(this=func_name, expressions=[this, pattern]),
4238                expressions=[exp.Slice(this=exp.Literal.number(1), expression=end)],
4239                offset=1,
4240            )
4241            transform = exp.Anonymous(
4242                this="LIST_TRANSFORM",
4243                expressions=[
4244                    lst,
4245                    exp.Lambda(
4246                        this=exp.Length(this=exp.to_identifier("x")),
4247                        expressions=[exp.to_identifier("x")],
4248                    ),
4249                ],
4250            )
4251            return exp.Coalesce(
4252                this=exp.Anonymous(this="LIST_SUM", expressions=[transform]),
4253                expressions=[exp.Literal.number(0)],
4254            )
4255
4256        # Position = 1 + sum(split_lengths[1:occ]) + sum(match_lengths[1:occ-1]) + offset
4257        base_pos: exp.Expr = (
4258            exp.Literal.number(1)
4259            + sum_lengths("STRING_SPLIT_REGEX", occurrence)
4260            + sum_lengths("REGEXP_EXTRACT_ALL", occurrence - exp.Literal.number(1))
4261            + pos_offset
4262        )
4263
4264        # option=1: add match length for end position
4265        if option and option.is_int and option.to_py() == 1:
4266            match_at_occ = exp.Bracket(
4267                this=exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern]),
4268                expressions=[occurrence],
4269                offset=1,
4270            )
4271            base_pos = base_pos + exp.Coalesce(
4272                this=exp.Length(this=match_at_occ), expressions=[exp.Literal.number(0)]
4273            )
4274
4275        # NULL checks for all provided arguments
4276        # .copy() is used strictly because .is_() alters the node's parent pointer, mutating the parsed AST
4277        null_args = [
4278            expression.this,
4279            expression.expression,
4280            position,
4281            orig_occ,
4282            option,
4283            parameters,
4284        ]
4285        null_checks = [arg.copy().is_(exp.Null()) for arg in null_args if arg]
4286
4287        matches = exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern])
4288
4289        return self.sql(
4290            exp.case()
4291            .when(exp.or_(*null_checks), exp.Null())
4292            .when(pattern.copy().eq(exp.Literal.string("")), exp.Literal.number(0))
4293            .when(exp.Length(this=matches) < occurrence, exp.Literal.number(0))
4294            .else_(base_pos)
4295        )
@unsupported_args('culture')
def numbertostr_sql(self, expression: sqlglot.expressions.string.NumberToStr) -> str:
4297    @unsupported_args("culture")
4298    def numbertostr_sql(self, expression: exp.NumberToStr) -> str:
4299        fmt = expression.args.get("format")
4300        if fmt and fmt.is_int:
4301            return self.func("FORMAT", f"'{{:,.{fmt.name}f}}'", expression.this)
4302
4303        self.unsupported("Only integer formats are supported by NumberToStr")
4304        return self.function_fallback_sql(expression)
def autoincrementcolumnconstraint_sql(self, _) -> str:
4306    def autoincrementcolumnconstraint_sql(self, _) -> str:
4307        self.unsupported("The AUTOINCREMENT column constraint is not supported by DuckDB")
4308        return ""
def aliases_sql(self, expression: sqlglot.expressions.core.Aliases) -> str:
4310    def aliases_sql(self, expression: exp.Aliases) -> str:
4311        this = expression.this
4312        if isinstance(this, exp.Posexplode):
4313            return self.posexplode_sql(this)
4314
4315        return super().aliases_sql(expression)
def posexplode_sql(self, expression: sqlglot.expressions.array.Posexplode) -> str:
4317    def posexplode_sql(self, expression: exp.Posexplode) -> str:
4318        this = expression.this
4319        parent = expression.parent
4320
4321        # The default Spark aliases are "pos" and "col", unless specified otherwise
4322        pos, col = exp.to_identifier("pos"), exp.to_identifier("col")
4323
4324        if isinstance(parent, exp.Aliases):
4325            # Column case: SELECT POSEXPLODE(col) [AS (a, b)]
4326            pos, col = parent.expressions
4327        elif isinstance(parent, exp.Table):
4328            # Table case: SELECT * FROM POSEXPLODE(col) [AS (a, b)]
4329            alias = parent.args.get("alias")
4330            if alias:
4331                pos, col = alias.columns or [pos, col]
4332                alias.pop()
4333
4334        # Translate POSEXPLODE to UNNEST + GENERATE_SUBSCRIPTS
4335        # Note: In Spark pos is 0-indexed, but in DuckDB it's 1-indexed, so we subtract 1 from GENERATE_SUBSCRIPTS
4336        unnest_sql = self.sql(exp.Unnest(expressions=[this], alias=col))
4337        gen_subscripts = self.sql(
4338            exp.Alias(
4339                this=exp.Anonymous(
4340                    this="GENERATE_SUBSCRIPTS", expressions=[this, exp.Literal.number(1)]
4341                )
4342                - exp.Literal.number(1),
4343                alias=pos,
4344            )
4345        )
4346
4347        posexplode_sql = self.format_args(gen_subscripts, unnest_sql)
4348
4349        if isinstance(parent, exp.From) or (parent and isinstance(parent.parent, exp.From)):
4350            # SELECT * FROM POSEXPLODE(col) -> SELECT * FROM (SELECT GENERATE_SUBSCRIPTS(...), UNNEST(...))
4351            return self.sql(exp.Subquery(this=exp.Select(expressions=[posexplode_sql])))
4352
4353        return posexplode_sql
def addmonths_sql(self, expression: sqlglot.expressions.temporal.AddMonths) -> str:
4355    def addmonths_sql(self, expression: exp.AddMonths) -> str:
4356        """
4357        Handles three key issues:
4358        1. Float/decimal months: e.g., Snowflake rounds, whereas DuckDB INTERVAL requires integers
4359        2. End-of-month preservation: If input is last day of month, result is last day of result month
4360        3. Type preservation: Maintains DATE/TIMESTAMPTZ types (DuckDB defaults to TIMESTAMP)
4361        """
4362        from sqlglot.optimizer.annotate_types import annotate_types
4363
4364        this = expression.this
4365        if not this.type:
4366            this = annotate_types(this, dialect=self.dialect)
4367
4368        if this.is_type(*exp.DataType.TEXT_TYPES):
4369            this = exp.Cast(this=this, to=exp.DataType(this=exp.DType.TIMESTAMP))
4370
4371        # Detect float/decimal months to apply rounding (Snowflake behavior)
4372        # DuckDB INTERVAL syntax doesn't support non-integer expressions, so use TO_MONTHS
4373        months_expr = expression.expression
4374        if not months_expr.type:
4375            months_expr = annotate_types(months_expr, dialect=self.dialect)
4376
4377        # Build interval or to_months expression based on type
4378        # Float/decimal case: Round and use TO_MONTHS(CAST(ROUND(value) AS INT))
4379        interval_or_to_months = (
4380            exp.func("TO_MONTHS", exp.cast(exp.func("ROUND", months_expr), "INT"))
4381            if months_expr.is_type(
4382                exp.DType.FLOAT,
4383                exp.DType.DOUBLE,
4384                exp.DType.DECIMAL,
4385            )
4386            # Integer case: standard INTERVAL N MONTH syntax
4387            else exp.Interval(this=months_expr, unit=exp.var("MONTH"))
4388        )
4389
4390        date_add_expr = exp.Add(this=this, expression=interval_or_to_months)
4391
4392        # Apply end-of-month preservation if Snowflake flag is set
4393        # CASE WHEN LAST_DAY(date) = date THEN LAST_DAY(result) ELSE result END
4394        preserve_eom = expression.args.get("preserve_end_of_month")
4395        result_expr = (
4396            exp.case()
4397            .when(
4398                exp.EQ(this=exp.func("LAST_DAY", this), expression=this),
4399                exp.func("LAST_DAY", date_add_expr),
4400            )
4401            .else_(date_add_expr)
4402            if preserve_eom
4403            else date_add_expr
4404        )
4405
4406        # DuckDB's DATE_ADD function returns TIMESTAMP/DATETIME by default, even when the input is DATE
4407        # To match for example Snowflake's ADD_MONTHS behavior (which preserves the input type)
4408        # We need to cast the result back to the original type when the input is DATE or TIMESTAMPTZ
4409        # Example: ADD_MONTHS('2023-01-31'::date, 1) should return DATE, not TIMESTAMP
4410        if this.is_type(exp.DType.DATE, exp.DType.TIMESTAMPTZ):
4411            return self.sql(exp.Cast(this=result_expr, to=this.type))
4412        return self.sql(result_expr)

Handles three key issues:

  1. Float/decimal months: e.g., Snowflake rounds, whereas DuckDB INTERVAL requires integers
  2. End-of-month preservation: If input is last day of month, result is last day of result month
  3. Type preservation: Maintains DATE/TIMESTAMPTZ types (DuckDB defaults to TIMESTAMP)
def format_sql(self, expression: sqlglot.expressions.string.Format) -> str:
4414    def format_sql(self, expression: exp.Format) -> str:
4415        if expression.name.lower() == "%s" and len(expression.expressions) == 1:
4416            return self.func("FORMAT", "'{}'", expression.expressions[0])
4417
4418        return self.function_fallback_sql(expression)
def hexstring_sql( self, expression: sqlglot.expressions.query.HexString, binary_function_repr: str | None = None) -> str:
4420    def hexstring_sql(
4421        self, expression: exp.HexString, binary_function_repr: str | None = None
4422    ) -> str:
4423        # UNHEX('FF') correctly produces blob \xFF in DuckDB
4424        return super().hexstring_sql(expression, binary_function_repr="UNHEX")
def datetrunc_sql(self, expression: sqlglot.expressions.temporal.DateTrunc) -> str:
4426    def datetrunc_sql(self, expression: exp.DateTrunc) -> str:
4427        unit = expression.args.get("unit")
4428        date = expression.this
4429
4430        week_start = _week_trunc_start_dow(unit)
4431        unit = unit_to_str(expression)
4432
4433        if week_start:
4434            result = self.sql(
4435                _build_week_trunc_expression(date, week_start, preserve_start_day=True)
4436            )
4437        else:
4438            result = self.func("DATE_TRUNC", unit, date)
4439
4440        if (
4441            expression.args.get("input_type_preserved")
4442            and date.is_type(*exp.DataType.TEMPORAL_TYPES)
4443            and not (is_date_unit(unit) and date.is_type(exp.DType.DATE))
4444        ):
4445            return self.sql(exp.Cast(this=result, to=date.type))
4446
4447        return result
def datetimetrunc_sql(self, expression: sqlglot.expressions.temporal.DatetimeTrunc) -> str:
4449    def datetimetrunc_sql(self, expression: exp.DatetimeTrunc) -> str:
4450        this = exp.cast(expression.this, exp.DType.DATETIME)
4451        week_start = _week_trunc_start_dow(expression.args.get("unit"))
4452        if week_start:
4453            return self.sql(
4454                _build_week_trunc_expression(
4455                    this, week_start, preserve_start_day=True, cast_to_date=False
4456                )
4457            )
4458
4459        return self.func("DATE_TRUNC", unit_to_str(expression), this)
def timestamptrunc_sql(self, expression: sqlglot.expressions.temporal.TimestampTrunc) -> str:
4461    def timestamptrunc_sql(self, expression: exp.TimestampTrunc) -> str:
4462        zone = expression.args.get("zone")
4463        timestamp = expression.this
4464        week_start = _week_trunc_start_dow(expression.args.get("unit"))
4465
4466        # The week start emulation below is exact, so avoid weekstart_unit_to_str's degrade warning
4467        unit = unit_to_str(expression) if week_start else weekstart_unit_to_str(self, expression)
4468        date_unit = is_date_unit(unit) or bool(week_start)
4469
4470        def _trunc_expr(this: exp.Expr) -> exp.Expr:
4471            if week_start:
4472                return _build_week_trunc_expression(
4473                    this, week_start, preserve_start_day=True, cast_to_date=False
4474                )
4475            return exp.func("DATE_TRUNC", unit, this)
4476
4477        if date_unit and zone:
4478            # BigQuery's TIMESTAMP_TRUNC with timezone truncates in the target timezone and returns as UTC.
4479            # Double AT TIME ZONE needed for BigQuery compatibility:
4480            # 1. First AT TIME ZONE: ensures truncation happens in the target timezone
4481            # 2. Second AT TIME ZONE: converts the DATE result back to TIMESTAMPTZ (preserving time component)
4482            timestamp = exp.AtTimeZone(this=timestamp, zone=zone)
4483            trunced = _trunc_expr(timestamp)
4484            if isinstance(trunced, exp.DateAdd):
4485                # Parenthesize so the trailing AT TIME ZONE binds to the whole shifted expression
4486                trunced = exp.Paren(this=trunced)
4487            return self.sql(exp.AtTimeZone(this=trunced, zone=zone))
4488
4489        result = self.sql(_trunc_expr(timestamp))
4490        if expression.args.get("input_type_preserved"):
4491            if timestamp.type and timestamp.is_type(exp.DType.TIME, exp.DType.TIMETZ):
4492                dummy_date = exp.Cast(
4493                    this=exp.Literal.string("1970-01-01"),
4494                    to=exp.DataType(this=exp.DType.DATE),
4495                )
4496                date_time = exp.Add(this=dummy_date, expression=timestamp)
4497                result = self.func("DATE_TRUNC", unit, date_time)
4498                return self.sql(exp.Cast(this=result, to=timestamp.type))
4499
4500            if timestamp.is_type(*exp.DataType.TEMPORAL_TYPES) and not (
4501                date_unit and timestamp.is_type(exp.DType.DATE)
4502            ):
4503                return self.sql(exp.Cast(this=result, to=timestamp.type))
4504
4505        return result
def trim_sql(self, expression: sqlglot.expressions.string.Trim) -> str:
4507    def trim_sql(self, expression: exp.Trim) -> str:
4508        expression.this.replace(_cast_to_varchar(expression.this))
4509        if expression.expression:
4510            expression.expression.replace(_cast_to_varchar(expression.expression))
4511
4512        result_sql = super().trim_sql(expression)
4513        return _gen_with_cast_to_blob(self, expression, result_sql)
def round_sql(self, expression: sqlglot.expressions.math.Round) -> str:
4515    def round_sql(self, expression: exp.Round) -> str:
4516        this = expression.this
4517        decimals = expression.args.get("decimals")
4518        truncate = expression.args.get("truncate")
4519
4520        # DuckDB requires the scale (decimals) argument to be an INT
4521        # Some dialects (e.g., Snowflake) allow non-integer scales and cast to an integer internally
4522        if decimals is not None and expression.args.get("casts_non_integer_decimals"):
4523            if not (decimals.is_int or decimals.is_type(*exp.DataType.INTEGER_TYPES)):
4524                decimals = exp.cast(decimals, exp.DType.INT)
4525
4526        func = "ROUND"
4527        if truncate:
4528            # BigQuery uses ROUND_HALF_EVEN; Snowflake uses HALF_TO_EVEN
4529            if truncate.this in ("ROUND_HALF_EVEN", "HALF_TO_EVEN"):
4530                func = "ROUND_EVEN"
4531                truncate = None
4532            # BigQuery uses ROUND_HALF_AWAY_FROM_ZERO; Snowflake uses HALF_AWAY_FROM_ZERO
4533            elif truncate.this in ("ROUND_HALF_AWAY_FROM_ZERO", "HALF_AWAY_FROM_ZERO"):
4534                truncate = None
4535
4536        return self.func(func, this, decimals, truncate)
def trycast_sql(self, expression: sqlglot.expressions.functions.TryCast) -> str:
4538    def trycast_sql(self, expression: exp.TryCast) -> str:
4539        to = expression.to
4540        to_type = to.this
4541        src = expression.this
4542
4543        if (
4544            expression.args.get("null_on_text_overflow")
4545            and to_type in exp.DataType.TEXT_TYPES
4546            and to.expressions
4547        ):
4548            return self.sql(
4549                exp.case()
4550                .when(
4551                    exp.LTE(this=exp.func("LENGTH", src), expression=to.expressions[0].this),
4552                    exp.cast(src, "TEXT"),
4553                )
4554                .else_(exp.Null())
4555            )
4556        elif to_type == exp.DType.DATE and expression.args.get("probe_date_format"):
4557            slash_strptime = exp.cast(
4558                exp.func("TRY_STRPTIME", src, exp.Literal.string(self._TRYCAST_DATE_SLASH_FMT)),
4559                "DATE",
4560            )
4561            mon_strptime = exp.cast(
4562                exp.func("TRY_STRPTIME", src, exp.Literal.string(self._TRYCAST_DATE_MON_FMT)),
4563                "DATE",
4564            )
4565            return self.sql(
4566                exp.case()
4567                .when(exp.func("CONTAINS", src, exp.Literal.string("/")), slash_strptime)
4568                .when(
4569                    exp.RegexpLike(this=src, expression=exp.Literal.string("[A-Za-z]")),
4570                    mon_strptime,
4571                )
4572                .else_(exp.TryCast(this=src, to=to))
4573            )
4574        elif (
4575            isinstance(to_type, exp.Interval)
4576            and (unit := to_type.unit)
4577            and expression.args.get("requires_string")
4578        ):
4579            interval_type = exp.DataType.build("INTERVAL")
4580            if isinstance(unit, exp.IntervalSpan):
4581                self.unsupported(
4582                    "TRY_CAST to INTERVAL with span (e.g. HOUR TO MINUTE) is not supported in DuckDB"
4583                )
4584                return self.sql(exp.TryCast(this=src, to=interval_type))
4585            return self.sql(
4586                exp.TryCast(
4587                    this=exp.DPipe(this=src, expression=exp.Literal.string(f" {unit.name}")),
4588                    to=interval_type,
4589                )
4590            )
4591
4592        return super().trycast_sql(expression)
def strtok_sql(self, expression: sqlglot.expressions.string.Strtok) -> str:
4594    def strtok_sql(self, expression: exp.Strtok) -> str:
4595        string_arg = expression.this
4596        delimiter_arg = expression.args.get("delimiter")
4597        part_index_arg = expression.args.get("part_index")
4598
4599        if delimiter_arg and part_index_arg:
4600            # Escape regex chars and build character class at runtime using REGEXP_REPLACE
4601            escaped_delimiter = exp.Anonymous(
4602                this="REGEXP_REPLACE",
4603                expressions=[
4604                    delimiter_arg,
4605                    exp.Literal.string(
4606                        r"([\[\]^.\-*+?(){}|$\\])"
4607                    ),  # Escape problematic regex chars
4608                    exp.Literal.string(
4609                        r"\\\1"
4610                    ),  # Replace with escaped version using $1 backreference
4611                    exp.Literal.string("g"),  # Global flag
4612                ],
4613            )
4614            # CASE WHEN delimiter = '' THEN '' ELSE CONCAT('[', escaped_delimiter, ']') END
4615            regex_pattern = (
4616                exp.case()
4617                .when(delimiter_arg.eq(exp.Literal.string("")), exp.Literal.string(""))
4618                .else_(
4619                    exp.func(
4620                        "CONCAT",
4621                        exp.Literal.string("["),
4622                        escaped_delimiter,
4623                        exp.Literal.string("]"),
4624                    )
4625                )
4626            )
4627
4628            # STRTOK skips empty strings, so we need to filter them out
4629            # LIST_FILTER(REGEXP_SPLIT_TO_ARRAY(string, pattern), x -> x != '')[index]
4630            split_array = exp.func("REGEXP_SPLIT_TO_ARRAY", string_arg, regex_pattern)
4631            x = exp.to_identifier("x")
4632            is_empty = x.eq(exp.Literal.string(""))
4633            filtered_array = exp.func(
4634                "LIST_FILTER",
4635                split_array,
4636                exp.Lambda(this=exp.not_(is_empty.copy()), expressions=[x.copy()]),
4637            )
4638            base_func = exp.Bracket(
4639                this=filtered_array,
4640                expressions=[part_index_arg],
4641                offset=1,
4642            )
4643
4644            # Use template with the built regex pattern
4645            result = exp.replace_placeholders(
4646                self.STRTOK_TEMPLATE.copy(),
4647                string=string_arg,
4648                delimiter=delimiter_arg,
4649                part_index=part_index_arg,
4650                base_func=base_func,
4651            )
4652
4653            return self.sql(result)
4654
4655        return self.function_fallback_sql(expression)
def strtoktoarray_sql(self, expression: sqlglot.expressions.array.StrtokToArray) -> str:
4657    def strtoktoarray_sql(self, expression: exp.StrtokToArray) -> str:
4658        string_arg = expression.this
4659        delimiter_arg = expression.args.get("expression") or exp.Literal.string(" ")
4660
4661        escaped = exp.RegexpReplace(
4662            this=delimiter_arg.copy(),
4663            expression=exp.Literal.string(r"([\[\]^.\-*+?(){}|$\\])"),
4664            replacement=exp.Literal.string(r"\\\1"),
4665            modifiers=exp.Literal.string("g"),
4666        )
4667        return self.sql(
4668            exp.replace_placeholders(
4669                self.STRTOK_TO_ARRAY_TEMPLATE.copy(),
4670                string=string_arg,
4671                delimiter=delimiter_arg,
4672                escaped=escaped,
4673            )
4674        )
def approxquantile_sql(self, expression: sqlglot.expressions.aggregate.ApproxQuantile) -> str:
4676    def approxquantile_sql(self, expression: exp.ApproxQuantile) -> str:
4677        result = self.func("APPROX_QUANTILE", expression.this, expression.args.get("quantile"))
4678
4679        # DuckDB returns integers for APPROX_QUANTILE, cast to DOUBLE if the expected type is a real type
4680        if expression.is_type(*exp.DataType.REAL_TYPES):
4681            result = f"CAST({result} AS DOUBLE)"
4682
4683        return result
def approxquantiles_sql(self, expression: sqlglot.expressions.aggregate.ApproxQuantiles) -> str:
4685    def approxquantiles_sql(self, expression: exp.ApproxQuantiles) -> str:
4686        """
4687        BigQuery's APPROX_QUANTILES(expr, n) returns an array of n+1 approximate quantile values
4688        dividing the input distribution into n equal-sized buckets.
4689
4690        Both BigQuery and DuckDB use approximate algorithms for quantile estimation, but BigQuery
4691        does not document the specific algorithm used so results may differ. DuckDB does not
4692        support RESPECT NULLS.
4693        """
4694        this = expression.this
4695        if isinstance(this, exp.Distinct):
4696            # APPROX_QUANTILES requires 2 args and DISTINCT node grabs both
4697            if len(this.expressions) < 2:
4698                self.unsupported("APPROX_QUANTILES requires a bucket count argument")
4699                return self.function_fallback_sql(expression)
4700            num_quantiles_expr = this.expressions[1].pop()
4701        else:
4702            num_quantiles_expr = expression.expression
4703
4704        if not isinstance(num_quantiles_expr, exp.Literal) or not num_quantiles_expr.is_int:
4705            self.unsupported("APPROX_QUANTILES bucket count must be a positive integer")
4706            return self.function_fallback_sql(expression)
4707
4708        num_quantiles = t.cast(int, num_quantiles_expr.to_py())
4709        if num_quantiles <= 0:
4710            self.unsupported("APPROX_QUANTILES bucket count must be a positive integer")
4711            return self.function_fallback_sql(expression)
4712
4713        quantiles = [
4714            exp.Literal.number(Decimal(i) / Decimal(num_quantiles))
4715            for i in range(num_quantiles + 1)
4716        ]
4717
4718        return self.sql(exp.ApproxQuantile(this=this, quantile=exp.Array(expressions=quantiles)))

BigQuery's APPROX_QUANTILES(expr, n) returns an array of n+1 approximate quantile values dividing the input distribution into n equal-sized buckets.

Both BigQuery and DuckDB use approximate algorithms for quantile estimation, but BigQuery does not document the specific algorithm used so results may differ. DuckDB does not support RESPECT NULLS.

def jsonextractscalar_sql(self, expression: sqlglot.expressions.json.JSONExtractScalar) -> str:
4720    def jsonextractscalar_sql(self, expression: exp.JSONExtractScalar) -> str:
4721        if expression.args.get("scalar_only"):
4722            expression = exp.JSONExtractScalar(
4723                this=rename_func("JSON_VALUE")(self, expression), expression="'$'"
4724            )
4725        return _arrow_json_extract_sql(self, expression)
def bitwisenot_sql(self, expression: sqlglot.expressions.core.BitwiseNot) -> str:
4727    def bitwisenot_sql(self, expression: exp.BitwiseNot) -> str:
4728        this = expression.this
4729
4730        if _is_binary(this):
4731            expression.type = exp.DType.BINARY.into_expr()
4732
4733        arg = _cast_to_bit(this)
4734
4735        if isinstance(this, exp.Neg):
4736            arg = exp.Paren(this=arg)
4737
4738        expression.set("this", arg)
4739
4740        result_sql = f"~{self.sql(expression, 'this')}"
4741
4742        return _gen_with_cast_to_blob(self, expression, result_sql)
def window_sql(self, expression: sqlglot.expressions.query.Window) -> str:
4744    def window_sql(self, expression: exp.Window) -> str:
4745        this = expression.this
4746        if isinstance(this, exp.Corr) or (
4747            isinstance(this, exp.Filter) and isinstance(this.this, exp.Corr)
4748        ):
4749            return self._corr_sql(expression)
4750
4751        return super().window_sql(expression)
def filter_sql(self, expression: sqlglot.expressions.core.Filter) -> str:
4753    def filter_sql(self, expression: exp.Filter) -> str:
4754        if isinstance(expression.this, exp.Corr):
4755            return self._corr_sql(expression)
4756
4757        return super().filter_sql(expression)
def uuid_sql(self, expression: sqlglot.expressions.functions.Uuid) -> str:
4776    def uuid_sql(self, expression: exp.Uuid) -> str:
4777        namespace = expression.this
4778        name = expression.args.get("name")
4779
4780        # UUID v5 (namespace + name) - Emulate using SHA1
4781        if namespace and name:
4782            result = exp.replace_placeholders(
4783                self.UUID_V5_TEMPLATE.copy(),
4784                namespace=namespace,
4785                name=name,
4786            )
4787            return self.sql(result)
4788
4789        return super().uuid_sql(expression)
Inherited Members
sqlglot.generator.Generator
Generator
NULL_ORDERING_SUPPORTED
WINDOW_FUNCS_WITH_NULL_ORDERING
LOCKING_READS_SUPPORTED
EXCEPT_INTERSECT_SUPPORT_ALL_CLAUSE
WRAP_DERIVED_VALUES
CREATE_FUNCTION_RETURN_AS
MATCHED_BY_SOURCE
SUPPORTS_MERGE_WHERE
SINGLE_STRING_INTERVAL
INTERVAL_ALLOWS_PLURAL_FORM
AUTO_REFRESH_BARE_INTERVALS
LIMIT_ONLY_LITERALS
GROUPINGS_SEP
INDEX_ON
INOUT_SEPARATOR
DIRECTED_JOINS
QUERY_HINT_SEP
IS_BOOL_ALLOWED
DUPLICATE_KEY_UPDATE_WITH_SET
LIMIT_IS_TOP
RETURNING_END
EXTRACT_ALLOWS_QUOTES
TZ_TO_WITH_TIME_ZONE
VALUES_AS_TABLE
ALTER_TABLE_INCLUDE_COLUMN_KEYWORD
UNNEST_WITH_ORDINALITY
COMPUTED_COLUMN_WITH_TYPE
SUPPORTS_TABLE_COPY
TABLESAMPLE_REQUIRES_PARENS
TABLESAMPLE_SIZE_IS_ROWS
TABLESAMPLE_WITH_METHOD
COLLATE_IS_FUNC
DATA_TYPE_SPECIFIERS_ALLOWED
ENSURE_BOOLS
CTE_RECURSIVE_KEYWORD_REQUIRED
SUPPORTS_SINGLE_ARG_CONCAT
SUPPORTS_TABLE_ALIAS_COLUMNS
SUPPORTS_NAMED_CTE_COLUMNS
UNPIVOT_ALIASES_ARE_IDENTIFIERS
PIVOT_ALIAS_WITH_AS
INSERT_OVERWRITE
SUPPORTS_SELECT_INTO
SUPPORTS_UNLOGGED_TABLES
SUPPORTS_MODIFY_COLUMN
SUPPORTS_CHANGE_COLUMN
LIKE_PROPERTY_INSIDE_SCHEMA
JSON_TYPE_REQUIRED_FOR_EXTRACTION
JSON_PATH_SINGLE_QUOTE_ESCAPE
JSON_PATH_KEY_QUOTED_FORCES_BRACKETS
SET_OP_MODIFIERS
COPY_PARAMS_ARE_WRAPPED
COPY_PARAMS_EQ_REQUIRED
TRY_SUPPORTED
SUPPORTS_UESCAPE
UNICODE_SUBSTITUTE
HEX_FUNC
WITH_PROPERTIES_PREFIX
QUOTE_JSON_PATH
SUPPORTS_EXPLODING_PROJECTIONS
ARRAY_CONCAT_IS_VAR_LEN
SUPPORTS_CONVERT_TIMEZONE
SUPPORTS_MEDIAN
SUPPORTS_UNIX_SECONDS
ALTER_SET_WRAPPED
PARSE_JSON_NAME
ARRAY_SIZE_NAME
ALTER_SET_TYPE
SUPPORTS_BETWEEN_FLAGS
MATCH_AGAINST_TABLE_PREFIX
DECLARE_DEFAULT_ASSIGNMENT
UPDATE_STATEMENT_SUPPORTS_FROM
STAR_EXCLUDE_REQUIRES_DERIVED_TABLE
UNSUPPORTED_TYPES
TIME_PART_SINGULARS
TOKEN_MAPPING
EXPRESSION_PRECEDES_PROPERTIES_CREATABLES
WITH_SEPARATED_COMMENTS
EXCLUDE_COMMENTS
PARAMETERIZABLE_TEXT_TYPES
EXPRESSIONS_WITHOUT_NESTED_CTES
RESPECT_IGNORE_NULLS_UNSUPPORTED_EXPRESSIONS
SAFE_JSON_PATH_KEY_RE
SENTINEL_LINE_BREAK
pretty
identify
normalize
pad
unsupported_level
max_unsupported
leading_comma
max_text_width
comments
dialect
normalize_functions
unsupported_messages
generate
preprocess
unsupported
sep
seg
sanitize_comment
maybe_comment
wrap
no_identify
normalize_func
indent
sql
uncache_sql
cache_sql
characterset_sql
column_parts
column_sql
pseudocolumn_sql
columnposition_sql
columndef_sql
columnconstraint_sql
computedcolumnconstraint_sql
compresscolumnconstraint_sql
generatedasidentitycolumnconstraint_sql
generatedasrowcolumnconstraint_sql
periodforsystemtimeconstraint_sql
notnullcolumnconstraint_sql
primarykeycolumnconstraint_sql
uniquecolumnconstraint_sql
inoutcolumnconstraint_sql
createable_sql
create_sql
sequenceproperties_sql
triggerproperties_sql
triggerreferencing_sql
triggerevent_sql
clone_sql
describe_sql
heredoc_sql
prepend_ctes
with_sql
cte_sql
tablealias_sql
bitstring_sql
bytestring_sql
unicodestring_sql
rawstring_sql
datatypeparam_sql
datatype_param_bound_limiter
datatype_sql
directory_sql
delete_sql
drop_sql
set_operation
set_operations
fetch_sql
limitoptions_sql
hint_sql
indexparameters_sql
index_sql
dynamicidentifier_sql
identifier_sql
lowerhex_sql
inputoutputformat_sql
national_sql
partition_sql
properties_sql
root_properties
properties
with_properties
locate_properties
property_name
property_sql
uuidproperty_sql
likeproperty_sql
fallbackproperty_sql
journalproperty_sql
freespaceproperty_sql
checksumproperty_sql
mergeblockratioproperty_sql
moduleproperty_sql
datablocksizeproperty_sql
blockcompressionproperty_sql
isolatedloadingproperty_sql
partitionboundspec_sql
partitionedofproperty_sql
lockingproperty_sql
withdataproperty_sql
withsystemversioningproperty_sql
insert_sql
introducer_sql
kill_sql
pseudotype_sql
objectidentifier_sql
onconflict_sql
returning_sql
rowformatdelimitedproperty_sql
withtablehint_sql
indextablehint_sql
historicaldata_sql
table_parts
table_sql
pivot_sql
version_sql
tuple_sql
update_sql
values_sql
var_sql
into_sql
from_sql
groupingsets_sql
rollup_sql
rollupindex_sql
rollupproperty_sql
cube_sql
group_sql
having_sql
connect_sql
prior_sql
lateral_op
lateral_sql
limit_sql
offset_sql
setitem_sql
set_sql
queryband_sql
pragma_sql
lock_sql
literal_sql
escape_str
loaddata_sql
null_sql
boolean_sql
booland_sql
boolor_sql
order_sql
withfill_sql
cluster_sql
clusterproperty_sql
distribute_sql
sort_sql
ordered_sql
matchrecognizemeasure_sql
matchrecognize_sql
query_modifiers
options_modifier
forclause_sql
queryoption_sql
offset_limit_modifiers
after_limit_modifiers
select_sql
schema_sql
schema_columns_sql
star_sql
parameter_sql
sessionparameter_sql
placeholder_sql
subquery_sql
qualify_sql
prewhere_sql
where_sql
partition_by_sql
windowspec_sql
between_sql
bracket_offset_expressions
all_sql
any_sql
exists_sql
case_sql
constraint_sql
nextvaluefor_sql
convert_concat_args
concat_sql
check_sql
foreignkey_sql
primarykey_sql
timeserieskey_sql
if_sql
matchagainst_sql
jsonkeyvalue_sql
jsonpath_sql
json_path_part
formatjson_sql
formatphrase_sql
jsonarray_sql
jsonarrayagg_sql
jsoncolumndef_sql
jsonschema_sql
jsontable_sql
openjsoncolumndef_sql
openjson_sql
in_unnest_op
interval_sql
return_sql
reference_sql
anonymous_sql
paren_sql
neg_sql
not_sql
alias_sql
pivotalias_sql
atindex_sql
attimezone_sql
fromtimezone_sql
fromiso8601date_sql
fromiso8601timestamp_sql
fromiso8601timestampnanos_sql
add_sql
and_sql
or_sql
xor_sql
connector_sql
bitwiseand_sql
bitwiseleftshift_sql
bitwiseor_sql
bitwiserightshift_sql
cast_sql
command_sql
comment_sql
mergetreettlaction_sql
mergetreettl_sql
transaction_sql
commit_sql
rollback_sql
altercolumn_sql
modifycolumn_sql
alterindex_sql
alterdiststyle_sql
altersortkey_sql
alterrename_sql
renamecolumn_sql
alterset_sql
alter_sql
altersession_sql
add_column_sql
droppartition_sql
dropprimarykey_sql
addconstraint_sql
addpartition_sql
distinct_sql
havingmax_sql
intdiv_sql
dpipe_sql
div_sql
safedivide_sql
overlaps_sql
distance_sql
distancend_sql
dot_sql
eq_sql
propertyeq_sql
escape_sql
glob_sql
gt_sql
gte_sql
is_sql
like_sql
ilike_sql
match_sql
similarto_sql
lt_sql
lte_sql
mod_sql
mul_sql
neq_sql
nullsafeeq_sql
nullsafeneq_sql
sub_sql
jsoncast_sql
try_sql
log_sql
use_sql
binary
ceil_floor
function_fallback_sql
func
format_args
too_wide
format_time
expressions
op_expressions
naked_property
tag_sql
token_sql
userdefinedfunction_sql
macrooverloads_sql
macrooverload_sql
joinhint_sql
kwarg_sql
when_sql
whens_sql
merge_sql
tochar_sql
dictproperty_sql
dictrange_sql
dictsubproperty_sql
duplicatekeyproperty_sql
uniquekeyproperty_sql
distributedbyproperty_sql
oncluster_sql
clusteredbyproperty_sql
anyvalue_sql
querytransform_sql
indexconstraintoption_sql
checkcolumnconstraint_sql
indexcolumnconstraint_sql
nvl2_sql
comprehension_sql
columnprefix_sql
opclass_sql
predict_sql
generateembedding_sql
generatetext_sql
generatetable_sql
generatebool_sql
generateint_sql
generatedouble_sql
mltranslate_sql
mlforecast_sql
aiforecast_sql
featuresattime_sql
vectorsearch_sql
forin_sql
refresh_sql
toarray_sql
tsordstotimestamp_sql
tsordstodatetime_sql
tsordstodate_sql
unixdate_sql
lastday_sql
dateadd_sql
arrayany_sql
struct_sql
partitionrange_sql
truncatetable_sql
convert_sql
copyparameter_sql
credentials_sql
copy_sql
semicolon_sql
datadeletionproperty_sql
maskingpolicycolumnconstraint_sql
gapfill_sql
scope_resolution
scoperesolution_sql
changes_sql
summarize_sql
explodinggenerateseries_sql
converttimezone_sql
json_sql
jsonvalue_sql
skipjsoncolumn_sql
conditionalinsert_sql
multitableinserts_sql
oncondition_sql
jsonextractquote_sql
jsonexists_sql
slice_sql
apply_sql
grant_sql
revoke_sql
grantprivilege_sql
grantprincipal_sql
columns_sql
overlay_sql
todouble_sql
string_sql
median_sql
overflowtruncatebehavior_sql
unixseconds_sql
arraysize_sql
attach_sql
detach_sql
attachoption_sql
watermarkcolumnconstraint_sql
encodeproperty_sql
includeproperty_sql
xmlelement_sql
xmlkeyvalueoption_sql
partitionbyrangeproperty_sql
partitionbyrangepropertydynamic_sql
unpivotcolumns_sql
analyzesample_sql
analyzestatistics_sql
analyzehistogram_sql
analyzedelete_sql
analyzelistchainedrows_sql
analyzevalidate_sql
analyze_sql
xmltable_sql
xmlnamespace_sql
export_sql
declare_sql
declareitem_sql
recursivewithsearch_sql
parameterizedagg_sql
anonymousaggfunc_sql
combinedaggfunc_sql
combinedparameterizedagg_sql
get_put_sql
translatecharacters_sql
decodecase_sql
semanticview_sql
getextract_sql
datefromunixdate_sql
buildproperty_sql
refreshtriggerproperty_sql
modelattribute_sql
directorystage_sql
initcap_sql
localtime_sql
localtimestamp_sql
weekstart_name
weekstart_sql
block_sql
functionspecification_sql
storedprocedure_sql
ifblock_sql
whileblock_sql
execute_sql
executesql_sql
altermodifysqlsecurity_sql
usingproperty_sql
renameindex_sql