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    arrow_json_extract_sql,
  14    array_append_sql,
  15    array_compact_sql,
  16    array_concat_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: t.ClassVar[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: t.ClassVar[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: t.ClassVar[exp.Expr] = exp.maybe_parse(
1979        "(ABS(HASH(:seed)) % 1000000) / 1000000.0"
1980    )
1981
1982    # Template for generating signed and unsigned SEQ values within a specified range
1983    SEQ_UNSIGNED: t.ClassVar[exp.Expr] = _SEQ_UNSIGNED
1984    SEQ_SIGNED: t.ClassVar[exp.Expr] = _SEQ_SIGNED
1985
1986    # Template for MAP_CAT transpilation - Snowflake semantics:
1987    # 1. Returns NULL if either input is NULL
1988    # 2. For duplicate keys, prefers non-NULL value (COALESCE(m2[k], m1[k]))
1989    # 3. Filters out entries with NULL values from the result
1990    MAPCAT_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
1991        """
1992        CASE
1993            WHEN :map1 IS NULL OR :map2 IS NULL THEN NULL
1994            ELSE MAP_FROM_ENTRIES(LIST_FILTER(LIST_TRANSFORM(
1995                LIST_DISTINCT(LIST_CONCAT(MAP_KEYS(:map1), MAP_KEYS(:map2))),
1996                __k -> STRUCT_PACK(key := __k, value := COALESCE(:map2[__k], :map1[__k]))
1997            ), __x -> __x.value IS NOT NULL))
1998        END
1999        """
2000    )
2001
2002    # Mappings for EXTRACT/DATE_PART transpilation
2003    # Maps Snowflake specifiers unsupported in DuckDB to strftime format codes
2004    EXTRACT_STRFTIME_MAPPINGS: t.ClassVar[dict[str, tuple[str, str]]] = {
2005        "WEEKISO": ("%V", "INTEGER"),
2006        "YEAROFWEEK": ("%G", "INTEGER"),
2007        "YEAROFWEEKISO": ("%G", "INTEGER"),
2008        "NANOSECOND": ("%n", "BIGINT"),
2009    }
2010
2011    # Maps epoch-based specifiers to DuckDB epoch functions
2012    EXTRACT_EPOCH_MAPPINGS: t.ClassVar[dict[str, str]] = {
2013        "EPOCH_SECOND": "EPOCH",
2014        "EPOCH_MILLISECOND": "EPOCH_MS",
2015        "EPOCH_MICROSECOND": "EPOCH_US",
2016        "EPOCH_NANOSECOND": "EPOCH_NS",
2017    }
2018
2019    # Template for BITMAP_CONSTRUCT_AGG transpilation
2020    #
2021    # BACKGROUND:
2022    # Snowflake's BITMAP_CONSTRUCT_AGG aggregates integers into a compact binary bitmap.
2023    # Supports values in range 0-32767, this version returns NULL if any value is out of range
2024    # See: https://docs.snowflake.com/en/sql-reference/functions/bitmap_construct_agg
2025    # See: https://docs.snowflake.com/en/user-guide/querying-bitmaps-for-distinct-counts
2026    #
2027    # Snowflake uses two different formats based on the number of unique values:
2028    #
2029    # Format 1 - Small bitmap (< 5 unique values): Length of 10 bytes
2030    #   Bytes 0-1: Count of values as 2-byte big-endian integer (e.g., 3 values = 0x0003)
2031    #   Bytes 2-9: Up to 4 values, each as 2-byte little-endian integers, zero-padded to 8 bytes
2032    #   Example: Values [1, 2, 3] -> 0x0003 0100 0200 0300 0000 (hex)
2033    #                                count  v1   v2   v3   pad
2034    #
2035    # Format 2 - Large bitmap (>= 5 unique values): Length of 10 + (2 * count) bytes
2036    #   Bytes 0-9: Fixed header 0x08 followed by 9 zero bytes
2037    #   Bytes 10+: Each value as 2-byte little-endian integer (no padding)
2038    #   Example: Values [1,2,3,4,5] -> 0x08 00000000 00000000 00 0100 0200 0300 0400 0500
2039    #                                  hdr  ----9 zero bytes----  v1   v2   v3   v4   v5
2040    #
2041    # TEMPLATE STRUCTURE
2042    #
2043    # Phase 1 - Innermost subquery: Data preparation
2044    #   SELECT LIST_SORT(...) AS l
2045    #   - Aggregates all input values into a list, remove NULLs, duplicates and sorts
2046    #   Result: Clean, sorted list of unique non-null integers stored as 'l'
2047    #
2048    # Phase 2 - Middle subquery: Hex string construction
2049    #   LIST_TRANSFORM(...)
2050    #   - Converts each integer to 2-byte little-endian hex representation
2051    #   - & 255 extracts low byte, >> 8 extracts high byte
2052    #   - LIST_REDUCE: Concatenates all hex pairs into single string 'h'
2053    #   Result: Hex string of all values
2054    #
2055    # Phase 3 - Outer SELECT: Final bitmap assembly
2056    #   LENGTH(l) < 5:
2057    #   - Small format: 2-byte count (big-endian via %04X) + values + zero padding
2058    #   LENGTH(l) >= 5:
2059    #   - Large format: Fixed 10-byte header + values (no padding needed)
2060    #   Result: Complete binary bitmap as BLOB
2061    #
2062    BITMAP_CONSTRUCT_AGG_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2063        """
2064        SELECT CASE
2065            WHEN l IS NULL OR LENGTH(l) = 0 THEN NULL
2066            WHEN LENGTH(l) != LENGTH(LIST_FILTER(l, __v -> __v BETWEEN 0 AND 32767)) THEN NULL
2067            WHEN LENGTH(l) < 5 THEN UNHEX(PRINTF('%04X', LENGTH(l)) || h || REPEAT('00', GREATEST(0, 4 - LENGTH(l)) * 2))
2068            ELSE UNHEX('08000000000000000000' || h)
2069        END
2070        FROM (
2071            SELECT l, COALESCE(LIST_REDUCE(
2072                LIST_TRANSFORM(l, __x -> PRINTF('%02X%02X', CAST(__x AS INT) & 255, (CAST(__x AS INT) >> 8) & 255)),
2073                (__a, __b) -> __a || __b, ''
2074            ), '') AS h
2075            FROM (SELECT LIST_SORT(LIST_DISTINCT(LIST(:arg) FILTER(NOT :arg IS NULL))) AS l)
2076        )
2077        """
2078    )
2079
2080    # Template for RANDSTR transpilation - placeholders get replaced with actual parameters
2081    RANDSTR_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2082        f"""
2083        SELECT LISTAGG(
2084            SUBSTRING(
2085                '{RANDSTR_CHAR_POOL}',
2086                1 + CAST(FLOOR(random_value * 62) AS INT),
2087                1
2088            ),
2089            ''
2090        )
2091        FROM (
2092            SELECT (ABS(HASH(i + :seed)) % 1000) / 1000.0 AS random_value
2093            FROM RANGE(:length) AS t(i)
2094        )
2095        """,
2096    )
2097
2098    # Template for MINHASH transpilation
2099    # Computes k minimum hash values across aggregated data using DuckDB list functions
2100    # Returns JSON matching Snowflake format: {"state": [...], "type": "minhash", "version": 1}
2101    MINHASH_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2102        """
2103        SELECT JSON_OBJECT('state', LIST(min_h ORDER BY seed), 'type', 'minhash', 'version', 1)
2104        FROM (
2105            SELECT seed, LIST_MIN(LIST_TRANSFORM(vals, __v -> HASH(CAST(__v AS VARCHAR) || CAST(seed AS VARCHAR)))) AS min_h
2106            FROM (SELECT LIST(:expr) AS vals), RANGE(0, :k) AS t(seed)
2107        )
2108        """,
2109    )
2110
2111    # Template for MINHASH_COMBINE transpilation
2112    # Combines multiple minhash signatures by taking element-wise minimum
2113    MINHASH_COMBINE_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2114        """
2115        SELECT JSON_OBJECT('state', LIST(min_h ORDER BY idx), 'type', 'minhash', 'version', 1)
2116        FROM (
2117            SELECT
2118                pos AS idx,
2119                MIN(val) AS min_h
2120            FROM
2121                UNNEST(LIST(:expr)) AS _(sig),
2122                UNNEST(CAST(sig -> 'state' AS UBIGINT[])) WITH ORDINALITY AS t(val, pos)
2123            GROUP BY pos
2124        )
2125        """,
2126    )
2127
2128    # Template for APPROXIMATE_SIMILARITY transpilation
2129    # Computes multi-way Jaccard similarity: fraction of positions where ALL signatures agree
2130    APPROXIMATE_SIMILARITY_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2131        """
2132        SELECT CAST(SUM(CASE WHEN num_distinct = 1 THEN 1 ELSE 0 END) AS DOUBLE) / COUNT(*)
2133        FROM (
2134            SELECT pos, COUNT(DISTINCT h) AS num_distinct
2135            FROM (
2136                SELECT h, pos
2137                FROM UNNEST(LIST(:expr)) AS _(sig),
2138                     UNNEST(CAST(sig -> 'state' AS UBIGINT[])) WITH ORDINALITY AS s(h, pos)
2139            )
2140            GROUP BY pos
2141        )
2142        """,
2143    )
2144
2145    # Template for ARRAYS_ZIP transpilation
2146    # Snowflake pads to longest array; DuckDB LIST_ZIP truncates to shortest
2147    # Uses RANGE + indexing to match Snowflake behavior
2148    ARRAYS_ZIP_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2149        """
2150        CASE WHEN :null_check THEN NULL
2151        WHEN :all_empty_check THEN [:empty_struct]
2152        ELSE LIST_TRANSFORM(RANGE(0, :max_len), __i -> :transform_struct)
2153        END
2154        """,
2155    )
2156
2157    UUID_V5_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2158        """
2159        (SELECT
2160            LOWER(
2161                SUBSTR(h, 1, 8) || '-' ||
2162                SUBSTR(h, 9, 4) || '-' ||
2163                '5' || SUBSTR(h, 14, 3) || '-' ||
2164                FORMAT('{:02x}', CAST('0x' || SUBSTR(h, 17, 2) AS INT) & 63 | 128) || SUBSTR(h, 19, 2) || '-' ||
2165                SUBSTR(h, 21, 12)
2166            )
2167        FROM (
2168            SELECT SUBSTR(SHA1(UNHEX(REPLACE(:namespace, '-', '')) || ENCODE(:name, 'utf8')), 1, 32) AS h
2169        ))
2170        """
2171    )
2172
2173    # Shared bag semantics outer frame for ARRAY_EXCEPT and ARRAY_INTERSECTION.
2174    # Each element is paired with its 1-based position via LIST_ZIP, then filtered
2175    # by a comparison operator (supplied via :cond) that determines the operation:
2176    #   EXCEPT (>):        keep the N-th occurrence only if N > count in arr2
2177    #                      e.g. [2,2,2] EXCEPT [2,2] -> [2]
2178    #   INTERSECTION (<=): keep the N-th occurrence only if N <= count in arr2
2179    #                      e.g. [2,2,2] INTERSECT [2,2] -> [2,2]
2180    # IS NOT DISTINCT FROM is used for NULL-safe element comparison.
2181    ARRAY_BAG_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2182        """
2183        CASE
2184            WHEN :arr1 IS NULL OR :arr2 IS NULL THEN NULL
2185            ELSE LIST_TRANSFORM(
2186                LIST_FILTER(
2187                    LIST_ZIP(:arr1, GENERATE_SERIES(1, LEN(:arr1))),
2188                    pair -> :cond
2189                ),
2190                pair -> pair[0]
2191            )
2192        END
2193        """
2194    )
2195
2196    ARRAY_EXCEPT_CONDITION: t.ClassVar[exp.Expr] = exp.maybe_parse(
2197        "LEN(LIST_FILTER(:arr1[1:pair[1]], e -> e IS NOT DISTINCT FROM pair[0]))"
2198        " > LEN(LIST_FILTER(:arr2, e -> e IS NOT DISTINCT FROM pair[0]))"
2199    )
2200
2201    ARRAY_INTERSECTION_CONDITION: t.ClassVar[exp.Expr] = exp.maybe_parse(
2202        "LEN(LIST_FILTER(:arr1[1:pair[1]], e -> e IS NOT DISTINCT FROM pair[0]))"
2203        " <= LEN(LIST_FILTER(:arr2, e -> e IS NOT DISTINCT FROM pair[0]))"
2204    )
2205
2206    # Set semantics for ARRAY_EXCEPT. Deduplicates arr1 via LIST_DISTINCT, then
2207    # filters out any element that appears at least once in arr2.
2208    #   e.g. [1,1,2,3] EXCEPT [1] -> [2,3]
2209    # IS NOT DISTINCT FROM is used for NULL-safe element comparison.
2210    ARRAY_EXCEPT_SET_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2211        """
2212        CASE
2213            WHEN :arr1 IS NULL OR :arr2 IS NULL THEN NULL
2214            ELSE LIST_FILTER(
2215                LIST_DISTINCT(:arr1),
2216                e -> LEN(LIST_FILTER(:arr2, x -> x IS NOT DISTINCT FROM e)) = 0
2217            )
2218        END
2219        """
2220    )
2221
2222    # BigQuery's `x IN UNNEST(arr)` NULL semantics:
2223    #   NULL IN UNNEST([1, 2])  -> NULL
2224    #   3 IN UNNEST([1, NULL])  -> NULL
2225    #   3 IN UNNEST([1, 2])     -> FALSE
2226    #   1 IN UNNEST(NULL)       -> FALSE (not NULL)
2227    #   1 IN UNNEST([])         -> FALSE
2228    # The default `IN (SELECT UNNEST(...))` rewrite creates a correlated subquery
2229    # that DuckDB rejects inside non-inner joins, so a CASE expression is used instead.
2230    IN_UNNEST_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2231        """
2232        CASE
2233            WHEN :arr IS NULL OR ARRAY_LENGTH(:arr) = 0 THEN FALSE
2234            WHEN ARRAY_CONTAINS(:arr, :value) THEN TRUE
2235            WHEN :value IS NULL OR ARRAY_LENGTH(:arr) <> LIST_COUNT(:arr) THEN NULL
2236            ELSE FALSE
2237        END
2238        """
2239    )
2240
2241    STRTOK_TO_ARRAY_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2242        """
2243        CASE WHEN :delimiter IS NULL THEN NULL
2244        ELSE LIST_FILTER(
2245            REGEXP_SPLIT_TO_ARRAY(:string, CASE WHEN :delimiter = '' THEN '.^' ELSE CONCAT('[', :escaped, ']') END),
2246            x -> NOT x = ''
2247        ) END
2248        """
2249    )
2250
2251    # Template for STRTOK function transpilation
2252    #
2253    # DuckDB itself doesn't have a strtok function. This handles the transpilation from Snowflake to DuckDB.
2254    # We may need to adjust this if we want to support transpilation from other dialects
2255    #
2256    # CASE
2257    #     -- Snowflake: empty delimiter + empty input string -> NULL
2258    #     WHEN delimiter = '' AND input_str = '' THEN NULL
2259    #
2260    #     -- Snowflake: empty delimiter + non-empty input string -> treats whole input as 1 token -> return input string if index is 1
2261    #     WHEN delimiter = '' AND index = 1 THEN input_str
2262    #
2263    #     -- Snowflake: empty delimiter + non-empty input string -> treats whole input as 1 token -> return NULL if index is not 1
2264    #     WHEN delimiter = '' THEN NULL
2265    #
2266    #     -- Snowflake: negative indices return NULL
2267    #     WHEN index < 0 THEN NULL
2268    #
2269    #     -- Snowflake: return NULL if any argument is NULL
2270    #     WHEN input_str IS NULL OR delimiter IS NULL OR index IS NULL THEN NULL
2271    #
2272    #
2273    #     ELSE LIST_FILTER(
2274    #         REGEXP_SPLIT_TO_ARRAY(
2275    #             input_str,
2276    #             CASE
2277    #                 -- if delimiter is '', we don't want to surround it with '[' and ']' as '[]' is invalid for DuckDB
2278    #                 WHEN delimiter = '' THEN ''
2279    #
2280    #                 -- handle problematic regex characters in delimiter with REGEXP_REPLACE
2281    #                 -- turn delimiter into a regex char set, otherwise DuckDB will match in order, which we don't want
2282    #                 ELSE '[' || REGEXP_REPLACE(delimiter, problematic_char_set, '\\\1', 'g') || ']'
2283    #             END
2284    #         ),
2285    #
2286    #         -- Snowflake: don't return empty strings
2287    #         x -> NOT x = ''
2288    #     )[index]
2289    # END
2290    STRTOK_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2291        """
2292        CASE
2293            WHEN :delimiter = '' AND :string = '' THEN NULL
2294            WHEN :delimiter = '' AND :part_index = 1 THEN :string
2295            WHEN :delimiter = '' THEN NULL
2296            WHEN :part_index < 0 THEN NULL
2297            WHEN :string IS NULL OR :delimiter IS NULL OR :part_index IS NULL THEN NULL
2298            ELSE :base_func
2299        END
2300        """
2301    )
2302
2303    # Snowflake AUTO detects 3 DATE formats: YYYY-MM-DD (ISO-8601), MM/DD/YYYY, DD-MON-YYYY.
2304    # DuckDB TRY_CAST handles ISO-8601 natively. For the other two formats we use CONTAINS('/')
2305    # and REGEXP_MATCHES('[A-Za-z]') as heuristics — these correctly handle single-digit months
2306    # and days (e.g. 1/5/2020, 5-JAN-2020) where a positional char check would fail.
2307    # Ref: https://docs.snowflake.com/en/sql-reference/date-time-input-output#date-formats
2308    _TRYCAST_DATE_SLASH_FMT = "%m/%d/%Y"
2309    _TRYCAST_DATE_MON_FMT = "%d-%b-%Y"
2310
2311    def _array_bag_sql(self, condition: exp.Expr, arr1: exp.Expr, arr2: exp.Expr) -> str:
2312        cond = exp.Paren(this=exp.replace_placeholders(condition, arr1=arr1, arr2=arr2))
2313        return self.sql(
2314            exp.replace_placeholders(self.ARRAY_BAG_TEMPLATE, arr1=arr1, arr2=arr2, cond=cond)
2315        )
2316
2317    def timeslice_sql(self, expression: exp.TimeSlice) -> str:
2318        """
2319        Transform Snowflake's TIME_SLICE to DuckDB's time_bucket.
2320
2321        Snowflake: TIME_SLICE(date_expr, slice_length, 'UNIT' [, 'START'|'END'])
2322        DuckDB:    time_bucket(INTERVAL 'slice_length' UNIT, date_expr)
2323
2324        For 'END' kind, add the interval to get the end of the slice.
2325        For DATE type with 'END', cast result back to DATE to preserve type.
2326        """
2327        date_expr = expression.this
2328        slice_length = expression.expression
2329        unit = expression.unit
2330        kind = expression.text("kind").upper()
2331
2332        # Create INTERVAL expression: INTERVAL 'N' UNIT
2333        interval_expr = exp.Interval(this=slice_length, unit=unit)
2334
2335        # Create base time_bucket expression
2336        time_bucket_expr = exp.func("time_bucket", interval_expr, date_expr)
2337
2338        # Check if we need the end of the slice (default is start)
2339        if not kind == "END":
2340            # For 'START', return time_bucket directly
2341            return self.sql(time_bucket_expr)
2342
2343        # For 'END', add the interval to get end of slice
2344        add_expr = exp.Add(this=time_bucket_expr, expression=interval_expr.copy())
2345
2346        # If input is DATE type, cast result back to DATE to preserve type
2347        # DuckDB converts DATE to TIMESTAMP when adding intervals
2348        if date_expr.is_type(exp.DType.DATE):
2349            return self.sql(exp.cast(add_expr, exp.DType.DATE))
2350
2351        return self.sql(add_expr)
2352
2353    def bitmapbucketnumber_sql(self, expression: exp.BitmapBucketNumber) -> str:
2354        """
2355        Transpile BITMAP_BUCKET_NUMBER function from Snowflake to DuckDB equivalent.
2356
2357        Snowflake's BITMAP_BUCKET_NUMBER returns a 1-based bucket identifier where:
2358        - Each bucket covers 32,768 values
2359        - Bucket numbering starts at 1
2360        - Formula: ((value - 1) // 32768) + 1 for positive values
2361
2362        For non-positive values (0 and negative), we use value // 32768 to avoid
2363        producing bucket 0 or positive bucket IDs for negative inputs.
2364        """
2365        value = expression.this
2366
2367        positive_formula = ((value - 1) // 32768) + 1
2368        non_positive_formula = value // 32768
2369
2370        # CASE WHEN value > 0 THEN ((value - 1) // 32768) + 1 ELSE value // 32768 END
2371        case_expr = (
2372            exp.case()
2373            .when(exp.GT(this=value, expression=exp.Literal.number(0)), positive_formula)
2374            .else_(non_positive_formula)
2375        )
2376        return self.sql(case_expr)
2377
2378    def bitmapbitposition_sql(self, expression: exp.BitmapBitPosition) -> str:
2379        """
2380        Transpile Snowflake's BITMAP_BIT_POSITION to DuckDB CASE expression.
2381
2382        Snowflake's BITMAP_BIT_POSITION behavior:
2383        - For n <= 0: returns ABS(n) % 32768
2384        - For n > 0: returns (n - 1) % 32768 (maximum return value is 32767)
2385        """
2386        this = expression.this
2387
2388        return self.sql(
2389            exp.Mod(
2390                this=exp.Paren(
2391                    this=exp.If(
2392                        this=exp.GT(this=this, expression=exp.Literal.number(0)),
2393                        true=this - exp.Literal.number(1),
2394                        false=exp.Abs(this=this),
2395                    )
2396                ),
2397                expression=MAX_BIT_POSITION,
2398            )
2399        )
2400
2401    def bitmapconstructagg_sql(self, expression: exp.BitmapConstructAgg) -> str:
2402        """
2403        Transpile Snowflake's BITMAP_CONSTRUCT_AGG to DuckDB equivalent.
2404        Uses a pre-parsed template with placeholders replaced by expression nodes.
2405
2406        Snowflake bitmap format:
2407        - Small (< 5 unique values): 2-byte count (big-endian) + values (little-endian) + padding to 10 bytes
2408        - Large (>= 5 unique values): 10-byte header (0x08 + 9 zeros) + values (little-endian)
2409        """
2410        arg = expression.this
2411        return (
2412            f"({self.sql(exp.replace_placeholders(self.BITMAP_CONSTRUCT_AGG_TEMPLATE, arg=arg))})"
2413        )
2414
2415    def getignorecase_sql(self, expression: exp.GetIgnoreCase) -> str:
2416        self.unsupported("DuckDB does not support the GET_IGNORE_CASE() function")
2417        return self.function_fallback_sql(expression)
2418
2419    def compress_sql(self, expression: exp.Compress) -> str:
2420        self.unsupported("DuckDB does not support the COMPRESS() function")
2421        return self.function_fallback_sql(expression)
2422
2423    def encrypt_sql(self, expression: exp.Encrypt) -> str:
2424        self.unsupported("ENCRYPT is not supported in DuckDB")
2425        return self.function_fallback_sql(expression)
2426
2427    def decrypt_sql(self, expression: exp.Decrypt) -> str:
2428        func_name = "TRY_DECRYPT" if expression.args.get("safe") else "DECRYPT"
2429        self.unsupported(f"{func_name} is not supported in DuckDB")
2430        return self.function_fallback_sql(expression)
2431
2432    def decryptraw_sql(self, expression: exp.DecryptRaw) -> str:
2433        func_name = "TRY_DECRYPT_RAW" if expression.args.get("safe") else "DECRYPT_RAW"
2434        self.unsupported(f"{func_name} is not supported in DuckDB")
2435        return self.function_fallback_sql(expression)
2436
2437    def encryptraw_sql(self, expression: exp.EncryptRaw) -> str:
2438        self.unsupported("ENCRYPT_RAW is not supported in DuckDB")
2439        return self.function_fallback_sql(expression)
2440
2441    def parseurl_sql(self, expression: exp.ParseUrl) -> str:
2442        self.unsupported("PARSE_URL is not supported in DuckDB")
2443        return self.function_fallback_sql(expression)
2444
2445    def parseip_sql(self, expression: exp.ParseIp) -> str:
2446        self.unsupported("PARSE_IP is not supported in DuckDB")
2447        return self.function_fallback_sql(expression)
2448
2449    def decompressstring_sql(self, expression: exp.DecompressString) -> str:
2450        self.unsupported("DECOMPRESS_STRING is not supported in DuckDB")
2451        return self.function_fallback_sql(expression)
2452
2453    def decompressbinary_sql(self, expression: exp.DecompressBinary) -> str:
2454        self.unsupported("DECOMPRESS_BINARY is not supported in DuckDB")
2455        return self.function_fallback_sql(expression)
2456
2457    def jarowinklersimilarity_sql(self, expression: exp.JarowinklerSimilarity) -> str:
2458        this = expression.this
2459        expr = expression.expression
2460
2461        if expression.args.get("case_insensitive"):
2462            this = exp.Upper(this=this)
2463            expr = exp.Upper(this=expr)
2464
2465        result = exp.func("JARO_WINKLER_SIMILARITY", this, expr)
2466
2467        if expression.args.get("integer_scale"):
2468            result = exp.cast(result * 100, "INTEGER")
2469
2470        return self.sql(result)
2471
2472    def randstr_sql(self, expression: exp.Randstr) -> str:
2473        """
2474        Transpile Snowflake's RANDSTR to DuckDB equivalent using deterministic hash-based random.
2475        Uses a pre-parsed template with placeholders replaced by expression nodes.
2476
2477        RANDSTR(length, generator) generates a random string of specified length.
2478        - With numeric seed: Use HASH(i + seed) for deterministic output (same seed = same result)
2479        - With RANDOM(): Use RANDOM() in the hash for non-deterministic output
2480        - No generator: Use default seed value
2481        """
2482        length = expression.this
2483        generator = expression.args.get("generator")
2484
2485        if generator:
2486            if isinstance(generator, exp.Rand):
2487                # If it's RANDOM(), use its seed if available, otherwise use RANDOM() itself
2488                seed_value = generator.this or generator
2489            else:
2490                # Const/int or other expression - use as seed directly
2491                seed_value = generator
2492        else:
2493            # No generator specified, use default seed (arbitrary but deterministic)
2494            seed_value = exp.Literal.number(RANDSTR_SEED)
2495
2496        replacements = {"seed": seed_value, "length": length}
2497        return f"({self.sql(exp.replace_placeholders(self.RANDSTR_TEMPLATE, **replacements))})"
2498
2499    @unsupported_args("finish")
2500    def reduce_sql(self, expression: exp.Reduce) -> str:
2501        array_arg = expression.this
2502        initial_value = expression.args.get("initial")
2503        merge_lambda = expression.args.get("merge")
2504
2505        if merge_lambda:
2506            merge_lambda.set("colon", True)
2507
2508        return self.func("list_reduce", array_arg, merge_lambda, initial_value)
2509
2510    def zipf_sql(self, expression: exp.Zipf) -> str:
2511        """
2512        Transpile Snowflake's ZIPF to DuckDB using CDF-based inverse sampling.
2513        Uses a pre-parsed template with placeholders replaced by expression nodes.
2514        """
2515        s = expression.this
2516        n = expression.args["elementcount"]
2517        gen = expression.args["gen"]
2518
2519        if not isinstance(gen, exp.Rand):
2520            # (ABS(HASH(seed)) % 1000000) / 1000000.0
2521            random_expr: exp.Expr = exp.Div(
2522                this=exp.Paren(
2523                    this=exp.Mod(
2524                        this=exp.Abs(this=exp.Anonymous(this="HASH", expressions=[gen.copy()])),
2525                        expression=exp.Literal.number(1000000),
2526                    )
2527                ),
2528                expression=exp.Literal.number(1000000.0),
2529            )
2530        else:
2531            # Use RANDOM() for non-deterministic output
2532            random_expr = exp.Rand()
2533
2534        replacements = {"s": s, "n": n, "random_expr": random_expr}
2535        return f"({self.sql(exp.replace_placeholders(self.ZIPF_TEMPLATE, **replacements))})"
2536
2537    def tobinary_sql(self, expression: exp.ToBinary) -> str:
2538        """
2539        TO_BINARY and TRY_TO_BINARY transpilation:
2540        - 'HEX': TO_BINARY('48454C50', 'HEX') -> UNHEX('48454C50')
2541        - 'UTF-8': TO_BINARY('TEST', 'UTF-8') -> ENCODE('TEST')
2542        - 'BASE64': TO_BINARY('SEVMUA==', 'BASE64') -> FROM_BASE64('SEVMUA==')
2543
2544        For TRY_TO_BINARY (safe=True), wrap with TRY():
2545        - 'HEX': TRY_TO_BINARY('invalid', 'HEX') -> TRY(UNHEX('invalid'))
2546        """
2547        value = expression.this
2548        format_arg = expression.args.get("format")
2549        is_safe = expression.args.get("safe")
2550        is_binary = _is_binary(expression)
2551
2552        if not format_arg and not is_binary:
2553            func_name = "TRY_TO_BINARY" if is_safe else "TO_BINARY"
2554            return self.func(func_name, value)
2555
2556        # Snowflake defaults to HEX encoding when no format is specified
2557        fmt = format_arg.name.upper() if format_arg else "HEX"
2558
2559        if fmt in ("UTF-8", "UTF8"):
2560            # DuckDB ENCODE always uses UTF-8, no charset parameter needed
2561            result = self.func("ENCODE", value)
2562        elif fmt == "BASE64":
2563            result = self.func("FROM_BASE64", value)
2564        elif fmt == "HEX":
2565            result = self.func("UNHEX", value)
2566        else:
2567            if is_safe:
2568                return self.sql(exp.null())
2569            else:
2570                self.unsupported(f"format {fmt} is not supported")
2571                result = self.func("TO_BINARY", value)
2572        return f"TRY({result})" if is_safe else result
2573
2574    def tonumber_sql(self, expression: exp.ToNumber) -> str:
2575        fmt = expression.args.get("format")
2576        precision = expression.args.get("precision")
2577        scale = expression.args.get("scale")
2578
2579        if not fmt and precision and scale:
2580            return self.sql(
2581                exp.cast(
2582                    expression.this, f"DECIMAL({precision.name}, {scale.name})", dialect="duckdb"
2583                )
2584            )
2585
2586        return super().tonumber_sql(expression)
2587
2588    def _greatest_least_sql(self, expression: exp.Greatest | exp.Least) -> str:
2589        """
2590        Handle GREATEST/LEAST functions with dialect-aware NULL behavior.
2591
2592        - If ignore_nulls=False (BigQuery-style): return NULL if any argument is NULL
2593        - If ignore_nulls=True (DuckDB/PostgreSQL-style): ignore NULLs, return greatest/least non-NULL value
2594        """
2595        # Get all arguments
2596        all_args = [expression.this, *expression.expressions]
2597        fallback_sql = self.function_fallback_sql(expression)
2598
2599        if expression.args.get("ignore_nulls"):
2600            # DuckDB/PostgreSQL behavior: use native GREATEST/LEAST (ignores NULLs)
2601            return self.sql(fallback_sql)
2602
2603        # return NULL if any argument is NULL
2604        case_expr = exp.case().when(
2605            exp.or_(*[arg.is_(exp.null()) for arg in all_args], copy=False),
2606            exp.null(),
2607            copy=False,
2608        )
2609        case_expr.set("default", fallback_sql)
2610        return self.sql(case_expr)
2611
2612    def generator_sql(self, expression: exp.Generator) -> str:
2613        # Transpile Snowflake GENERATOR to DuckDB range()
2614        rowcount = expression.args.get("rowcount")
2615        time_limit = expression.args.get("time_limit")
2616
2617        if time_limit:
2618            self.unsupported("GENERATOR TIMELIMIT parameter is not supported in DuckDB")
2619
2620        if not rowcount:
2621            self.unsupported("GENERATOR without ROWCOUNT is not supported in DuckDB")
2622            return self.func("range", exp.Literal.number(0))
2623
2624        return self.func("range", rowcount)
2625
2626    def greatest_sql(self, expression: exp.Greatest) -> str:
2627        return self._greatest_least_sql(expression)
2628
2629    def least_sql(self, expression: exp.Least) -> str:
2630        return self._greatest_least_sql(expression)
2631
2632    def lambda_sql(self, expression: exp.Lambda, arrow_sep: str = "->", wrap: bool = True) -> str:
2633        if expression.args.get("colon"):
2634            prefix = "LAMBDA "
2635            arrow_sep = ":"
2636            wrap = False
2637        else:
2638            prefix = ""
2639
2640        lambda_sql = super().lambda_sql(expression, arrow_sep=arrow_sep, wrap=wrap)
2641        return f"{prefix}{lambda_sql}"
2642
2643    def show_sql(self, expression: exp.Show) -> str:
2644        from_ = self.sql(expression, "from_")
2645        from_ = f" FROM {from_}" if from_ else ""
2646        return f"SHOW {expression.name}{from_}"
2647
2648    def soundex_sql(self, expression: exp.Soundex) -> str:
2649        self.unsupported("SOUNDEX is not supported in DuckDB")
2650        return self.func("SOUNDEX", expression.this)
2651
2652    def sortarray_sql(self, expression: exp.SortArray) -> str:
2653        arr = expression.this
2654        asc = expression.args.get("asc")
2655        nulls_first = expression.args.get("nulls_first")
2656
2657        if not isinstance(asc, exp.Boolean) and not isinstance(nulls_first, exp.Boolean):
2658            return self.func("LIST_SORT", arr, asc, nulls_first)
2659
2660        nulls_are_first = nulls_first == exp.true()
2661        nulls_first_sql = exp.Literal.string("NULLS FIRST") if nulls_are_first else None
2662
2663        if not isinstance(asc, exp.Boolean):
2664            return self.func("LIST_SORT", arr, asc, nulls_first_sql)
2665
2666        descending = asc == exp.false()
2667
2668        if not descending and not nulls_are_first:
2669            return self.func("LIST_SORT", arr)
2670        if not nulls_are_first:
2671            return self.func("ARRAY_REVERSE_SORT", arr)
2672        return self.func(
2673            "LIST_SORT",
2674            arr,
2675            exp.Literal.string("DESC" if descending else "ASC"),
2676            exp.Literal.string("NULLS FIRST"),
2677        )
2678
2679    def install_sql(self, expression: exp.Install) -> str:
2680        force = "FORCE " if expression.args.get("force") else ""
2681        this = self.sql(expression, "this")
2682        from_clause = expression.args.get("from_")
2683        from_clause = f" FROM {from_clause}" if from_clause else ""
2684        return f"{force}INSTALL {this}{from_clause}"
2685
2686    def approxtopk_sql(self, expression: exp.ApproxTopK) -> str:
2687        self.unsupported(
2688            "APPROX_TOP_K cannot be transpiled to DuckDB due to incompatible return types. "
2689        )
2690        return self.function_fallback_sql(expression)
2691
2692    def strposition_sql(self, expression: exp.StrPosition) -> str:
2693        this = expression.this
2694        substr = expression.args.get("substr")
2695        position = expression.args.get("position")
2696
2697        # For BINARY/BLOB: DuckDB's STRPOS doesn't support BLOB types
2698        # Convert to HEX strings, use STRPOS, then convert hex position to byte position
2699        if _is_binary(this):
2700            # Build expression: STRPOS(HEX(haystack), HEX(needle))
2701            hex_strpos = exp.StrPosition(
2702                this=exp.Hex(this=this),
2703                substr=exp.Hex(this=substr),
2704            )
2705
2706            return self.sql(exp.cast((hex_strpos + 1) / 2, exp.DType.INT))
2707
2708        # For VARCHAR: handle clamp_position
2709        if expression.args.get("clamp_position") and position:
2710            expression = expression.copy()
2711            expression.set(
2712                "position",
2713                exp.If(
2714                    this=exp.LTE(this=position, expression=exp.Literal.number(0)),
2715                    true=exp.Literal.number(1),
2716                    false=position.copy(),
2717                ),
2718            )
2719
2720        return strposition_sql(self, expression)
2721
2722    def substring_sql(self, expression: exp.Substring) -> str:
2723        if expression.args.get("zero_start"):
2724            start = expression.args.get("start")
2725            length = expression.args.get("length")
2726
2727            if start := expression.args.get("start"):
2728                start = exp.If(this=start.eq(0), true=exp.Literal.number(1), false=start)
2729            if length := expression.args.get("length"):
2730                length = exp.If(this=length < 0, true=exp.Literal.number(0), false=length)
2731
2732            return self.func("SUBSTRING", expression.this, start, length)
2733
2734        return self.function_fallback_sql(expression)
2735
2736    def strtotime_sql(self, expression: exp.StrToTime) -> str:
2737        # Check if target_type requires TIMESTAMPTZ (for LTZ/TZ variants)
2738        target_type = expression.args.get("target_type")
2739        needs_tz = target_type and target_type.this in (
2740            exp.DType.TIMESTAMPLTZ,
2741            exp.DType.TIMESTAMPTZ,
2742        )
2743
2744        value, formatted_time = self._strptime_default_year(expression)
2745
2746        if expression.args.get("safe"):
2747            cast_type = exp.DType.TIMESTAMPTZ if needs_tz else exp.DType.TIMESTAMP
2748            return self.sql(exp.cast(self.func("TRY_STRPTIME", value, formatted_time), cast_type))
2749
2750        base_sql = self.func("STRPTIME", value, formatted_time)
2751        if needs_tz:
2752            return self.sql(
2753                exp.cast(
2754                    base_sql,
2755                    exp.DataType(this=exp.DType.TIMESTAMPTZ),
2756                )
2757            )
2758        return base_sql
2759
2760    def strtodate_sql(self, expression: exp.StrToDate) -> str:
2761        value, formatted_time = self._strptime_default_year(expression)
2762        function_name = "STRPTIME" if not expression.args.get("safe") else "TRY_STRPTIME"
2763        return self.sql(
2764            exp.cast(
2765                self.func(function_name, value, formatted_time),
2766                exp.DataType(this=exp.DType.DATE),
2767            )
2768        )
2769
2770    def _strptime_default_year(
2771        self, expression: exp.StrToTime | exp.StrToDate | exp.ParseDatetime
2772    ) -> tuple[exp.ExpOrStr, exp.ExpOrStr | None]:
2773        value: exp.ExpOrStr = expression.this
2774        formatted_time: exp.ExpOrStr | None = self.format_time(expression)
2775
2776        if default_year := expression.args.get("default_year"):
2777            value = exp.DPipe(this=exp.Literal.string(f"{default_year.name} "), expression=value)
2778            formatted_time = exp.DPipe(this=exp.Literal.string("%Y "), expression=formatted_time)
2779
2780        return value, formatted_time
2781
2782    def parsedatetime_sql(self, expression: exp.ParseDatetime) -> str:
2783        value, formatted_time = self._strptime_default_year(expression)
2784        return self.func("STRPTIME", value, formatted_time)
2785
2786    def parsetime_sql(self, expression: exp.ParseTime) -> str:
2787        formatted_time = self.format_time(expression)
2788        return self.sql(
2789            exp.cast(
2790                self.func("STRPTIME", expression.this, formatted_time),
2791                exp.DataType(this=exp.DType.TIME),
2792            )
2793        )
2794
2795    def tsordstotime_sql(self, expression: exp.TsOrDsToTime) -> str:
2796        this = expression.this
2797        time_format = self.format_time(expression)
2798        safe = expression.args.get("safe")
2799        time_type = exp.DataType.from_str("TIME", dialect="duckdb")
2800        cast_expr = exp.TryCast if safe else exp.Cast
2801
2802        if time_format:
2803            func_name = "TRY_STRPTIME" if safe else "STRPTIME"
2804            strptime = exp.Anonymous(this=func_name, expressions=[this, time_format])
2805            return self.sql(cast_expr(this=strptime, to=time_type))
2806
2807        if isinstance(this, exp.TsOrDsToTime) or this.is_type(exp.DType.TIME):
2808            return self.sql(this)
2809
2810        return self.sql(cast_expr(this=this, to=time_type))
2811
2812    def currentdate_sql(self, expression: exp.CurrentDate) -> str:
2813        if not expression.this:
2814            return "CURRENT_DATE"
2815
2816        expr = exp.Cast(
2817            this=exp.AtTimeZone(this=exp.CurrentTimestamp(), zone=expression.this),
2818            to=exp.DataType(this=exp.DType.DATE),
2819        )
2820        return self.sql(expr)
2821
2822    def checkjson_sql(self, expression: exp.CheckJson) -> str:
2823        arg = expression.this
2824        return self.sql(
2825            exp.case()
2826            .when(
2827                exp.or_(arg.is_(exp.Null()), arg.eq(""), exp.func("json_valid", arg)),
2828                exp.null(),
2829            )
2830            .else_(exp.Literal.string("Invalid JSON"))
2831        )
2832
2833    def parsejson_sql(self, expression: exp.ParseJSON) -> str:
2834        arg = expression.this
2835        if expression.args.get("safe"):
2836            return self.sql(
2837                exp.case()
2838                .when(exp.func("json_valid", arg), exp.cast(arg.copy(), "JSON"))
2839                .else_(exp.null())
2840            )
2841        return self.func("JSON", arg)
2842
2843    def unicode_sql(self, expression: exp.Unicode) -> str:
2844        if expression.args.get("empty_is_zero"):
2845            return self.sql(
2846                exp.case()
2847                .when(expression.this.eq(exp.Literal.string("")), exp.Literal.number(0))
2848                .else_(exp.Anonymous(this="UNICODE", expressions=[expression.this]))
2849            )
2850
2851        return self.func("UNICODE", expression.this)
2852
2853    def stripnullvalue_sql(self, expression: exp.StripNullValue) -> str:
2854        return self.sql(
2855            exp.case()
2856            .when(exp.func("json_type", expression.this).eq("NULL"), exp.null())
2857            .else_(expression.this)
2858        )
2859
2860    def trunc_sql(self, expression: exp.Trunc) -> str:
2861        decimals = expression.args.get("decimals")
2862        if (
2863            expression.args.get("fractions_supported")
2864            and decimals
2865            and not decimals.is_type(exp.DType.INT)
2866        ):
2867            decimals = exp.cast(decimals, exp.DType.INT, dialect="duckdb")
2868
2869        return self.func("TRUNC", expression.this, decimals)
2870
2871    def normal_sql(self, expression: exp.Normal) -> str:
2872        """
2873        Transpile Snowflake's NORMAL(mean, stddev, gen) to DuckDB.
2874
2875        Uses the Box-Muller transform via NORMAL_TEMPLATE.
2876        """
2877        mean = expression.this
2878        stddev = expression.args["stddev"]
2879        gen: exp.Expr = expression.args["gen"]
2880
2881        # Build two uniform random values [0, 1) for Box-Muller transform
2882        if isinstance(gen, exp.Rand) and gen.this is None:
2883            u1: exp.Expr = exp.Rand()
2884            u2: exp.Expr = exp.Rand()
2885        else:
2886            # Seeded: derive two values using HASH with different inputs
2887            seed = gen.this if isinstance(gen, exp.Rand) else gen
2888            u1 = exp.replace_placeholders(self.SEEDED_RANDOM_TEMPLATE, seed=seed)
2889            u2 = exp.replace_placeholders(
2890                self.SEEDED_RANDOM_TEMPLATE,
2891                seed=exp.Add(this=seed.copy(), expression=exp.Literal.number(1)),
2892            )
2893
2894        replacements = {"mean": mean, "stddev": stddev, "u1": u1, "u2": u2}
2895        return self.sql(exp.replace_placeholders(self.NORMAL_TEMPLATE, **replacements))
2896
2897    def uniform_sql(self, expression: exp.Uniform) -> str:
2898        """
2899        Transpile Snowflake's UNIFORM(min, max, gen) to DuckDB.
2900
2901        UNIFORM returns a random value in [min, max]:
2902        - Integer result if both min and max are integers
2903        - Float result if either min or max is a float
2904        """
2905        min_val = expression.this
2906        max_val = expression.expression
2907        gen = expression.args.get("gen")
2908
2909        # Determine if result should be integer (both bounds are integers).
2910        # We do this to emulate Snowflake's behavior, INT -> INT, FLOAT -> FLOAT
2911        is_int_result = min_val.is_int and max_val.is_int
2912
2913        # Build the random value expression [0, 1)
2914        if not isinstance(gen, exp.Rand):
2915            # Seed value: (ABS(HASH(seed)) % 1000000) / 1000000.0
2916            random_expr: exp.Expr = exp.Div(
2917                this=exp.Paren(
2918                    this=exp.Mod(
2919                        this=exp.Abs(this=exp.Anonymous(this="HASH", expressions=[gen])),
2920                        expression=exp.Literal.number(1000000),
2921                    )
2922                ),
2923                expression=exp.Literal.number(1000000.0),
2924            )
2925        else:
2926            random_expr = exp.Rand()
2927
2928        # Build: min + random * (max - min [+ 1 for int])
2929        range_expr: exp.Expr = exp.Sub(this=max_val, expression=min_val)
2930        if is_int_result:
2931            range_expr = exp.Add(this=range_expr, expression=exp.Literal.number(1))
2932
2933        result: exp.Expr = exp.Add(
2934            this=min_val,
2935            expression=exp.Mul(this=random_expr, expression=exp.Paren(this=range_expr)),
2936        )
2937
2938        if is_int_result:
2939            result = exp.Cast(this=exp.Floor(this=result), to=exp.DType.BIGINT.into_expr())
2940
2941        return self.sql(result)
2942
2943    def timefromparts_sql(self, expression: exp.TimeFromParts) -> str:
2944        nano = expression.args.get("nano")
2945        overflow = expression.args.get("overflow")
2946
2947        # Snowflake's TIME_FROM_PARTS supports overflow
2948        if overflow:
2949            hour = expression.args["hour"]
2950            minute = expression.args["min"]
2951            sec = expression.args["sec"]
2952
2953            # Check if values are within normal ranges - use MAKE_TIME for efficiency
2954            if not nano and all(arg.is_int for arg in [hour, minute, sec]):
2955                try:
2956                    h_val = hour.to_py()
2957                    m_val = minute.to_py()
2958                    s_val = sec.to_py()
2959                    if 0 <= h_val <= 23 and 0 <= m_val <= 59 and 0 <= s_val <= 59:
2960                        return rename_func("MAKE_TIME")(self, expression)
2961                except ValueError:
2962                    pass
2963
2964            # Overflow or nanoseconds detected - use INTERVAL arithmetic
2965            if nano:
2966                sec = sec + nano.pop() / exp.Literal.number(1000000000.0)
2967
2968            total_seconds = hour * exp.Literal.number(3600) + minute * exp.Literal.number(60) + sec
2969
2970            return self.sql(
2971                exp.Add(
2972                    this=exp.Cast(
2973                        this=exp.Literal.string("00:00:00"), to=exp.DType.TIME.into_expr()
2974                    ),
2975                    expression=exp.Interval(this=total_seconds, unit=exp.var("SECOND")),
2976                )
2977            )
2978
2979        # Default: MAKE_TIME
2980        if nano:
2981            expression.set(
2982                "sec", expression.args["sec"] + nano.pop() / exp.Literal.number(1000000000.0)
2983            )
2984
2985        return rename_func("MAKE_TIME")(self, expression)
2986
2987    def extract_sql(self, expression: exp.Extract) -> str:
2988        """
2989        Transpile EXTRACT/DATE_PART for DuckDB, handling specifiers not natively supported.
2990
2991        DuckDB doesn't support: WEEKISO, YEAROFWEEK, YEAROFWEEKISO, NANOSECOND,
2992        EPOCH_SECOND (as integer), EPOCH_MILLISECOND, EPOCH_MICROSECOND, EPOCH_NANOSECOND
2993        """
2994        this = expression.this
2995        datetime_expr = expression.expression
2996
2997        # TIMESTAMPTZ extractions may produce different results between Snowflake and DuckDB
2998        # because Snowflake applies server timezone while DuckDB uses local timezone
2999        if datetime_expr.is_type(exp.DType.TIMESTAMPTZ, exp.DType.TIMESTAMPLTZ):
3000            self.unsupported(
3001                "EXTRACT from TIMESTAMPTZ / TIMESTAMPLTZ may produce different results due to timezone handling differences"
3002            )
3003
3004        part_name = this.name.upper()
3005
3006        if part_name in self.EXTRACT_STRFTIME_MAPPINGS:
3007            fmt, cast_type = self.EXTRACT_STRFTIME_MAPPINGS[part_name]
3008
3009            # Problem: strftime doesn't accept TIME and there's no NANOSECOND function
3010            # So, for NANOSECOND with TIME, fallback to MICROSECOND * 1000
3011            is_nano_time = part_name == "NANOSECOND" and datetime_expr.is_type(
3012                exp.DType.TIME, exp.DType.TIMETZ
3013            )
3014
3015            if is_nano_time:
3016                self.unsupported("Parameter NANOSECOND is not supported with TIME type in DuckDB")
3017                return self.sql(
3018                    exp.cast(
3019                        exp.Mul(
3020                            this=exp.Extract(this=exp.var("MICROSECOND"), expression=datetime_expr),
3021                            expression=exp.Literal.number(1000),
3022                        ),
3023                        exp.DataType.from_str(cast_type, dialect="duckdb"),
3024                    )
3025                )
3026
3027            # For NANOSECOND, cast to TIMESTAMP_NS to preserve nanosecond precision
3028            strftime_input = datetime_expr
3029            if part_name == "NANOSECOND":
3030                strftime_input = exp.cast(datetime_expr, exp.DType.TIMESTAMP_NS)
3031
3032            return self.sql(
3033                exp.cast(
3034                    exp.Anonymous(
3035                        this="STRFTIME",
3036                        expressions=[strftime_input, exp.Literal.string(fmt)],
3037                    ),
3038                    exp.DataType.from_str(cast_type, dialect="duckdb"),
3039                )
3040            )
3041
3042        if part_name in self.EXTRACT_EPOCH_MAPPINGS:
3043            func_name = self.EXTRACT_EPOCH_MAPPINGS[part_name]
3044            result: exp.Expr = exp.Anonymous(this=func_name, expressions=[datetime_expr])
3045            # EPOCH returns float, cast to BIGINT for integer result
3046            if part_name == "EPOCH_SECOND":
3047                result = exp.cast(result, exp.DataType.from_str("BIGINT", dialect="duckdb"))
3048            return self.sql(result)
3049
3050        return super().extract_sql(expression)
3051
3052    def timestampfromparts_sql(self, expression: exp.TimestampFromParts) -> str:
3053        # Check if this is the date/time expression form: TIMESTAMP_FROM_PARTS(date_expr, time_expr)
3054        date_expr = expression.this
3055        time_expr = expression.expression
3056
3057        if date_expr is not None and time_expr is not None:
3058            # In DuckDB, DATE + TIME produces TIMESTAMP
3059            return self.sql(exp.Add(this=date_expr, expression=time_expr))
3060
3061        # Component-based form: TIMESTAMP_FROM_PARTS(year, month, day, hour, minute, second, ...)
3062        sec = expression.args.get("sec")
3063        if sec is None:
3064            # This shouldn't happen with valid input, but handle gracefully
3065            return rename_func("MAKE_TIMESTAMP")(self, expression)
3066
3067        milli = expression.args.get("milli")
3068        if milli is not None:
3069            sec += milli.pop() / exp.Literal.number(1000.0)
3070
3071        nano = expression.args.get("nano")
3072        if nano is not None:
3073            sec += nano.pop() / exp.Literal.number(1000000000.0)
3074
3075        if milli or nano:
3076            expression.set("sec", sec)
3077
3078        return rename_func("MAKE_TIMESTAMP")(self, expression)
3079
3080    @unsupported_args("nano")
3081    def timestampltzfromparts_sql(self, expression: exp.TimestampLtzFromParts) -> str:
3082        # Pop nano so rename_func only passes args that MAKE_TIMESTAMP accepts
3083        if nano := expression.args.get("nano"):
3084            nano.pop()
3085
3086        timestamp = rename_func("MAKE_TIMESTAMP")(self, expression)
3087        return f"CAST({timestamp} AS TIMESTAMPTZ)"
3088
3089    @unsupported_args("nano")
3090    def timestamptzfromparts_sql(self, expression: exp.TimestampTzFromParts) -> str:
3091        # Extract zone before popping
3092        zone = expression.args.get("zone")
3093        # Pop zone and nano so rename_func only passes args that MAKE_TIMESTAMP accepts
3094        if zone:
3095            zone = zone.pop()
3096
3097        if nano := expression.args.get("nano"):
3098            nano.pop()
3099
3100        timestamp = rename_func("MAKE_TIMESTAMP")(self, expression)
3101
3102        if zone:
3103            # Use AT TIME ZONE to apply the explicit timezone
3104            return f"{timestamp} AT TIME ZONE {self.sql(zone)}"
3105
3106        return timestamp
3107
3108    def tablesample_sql(
3109        self,
3110        expression: exp.TableSample,
3111        tablesample_keyword: str | None = None,
3112    ) -> str:
3113        if not isinstance(expression.parent, exp.Select):
3114            # This sample clause only applies to a single source, not the entire resulting relation
3115            tablesample_keyword = "TABLESAMPLE"
3116
3117        if expression.args.get("size"):
3118            method = expression.args.get("method")
3119            if method and method.name.upper() != "RESERVOIR":
3120                self.unsupported(
3121                    f"Sampling method {method} is not supported with a discrete sample count, "
3122                    "defaulting to reservoir sampling"
3123                )
3124                expression.set("method", exp.var("RESERVOIR"))
3125
3126        return super().tablesample_sql(expression, tablesample_keyword=tablesample_keyword)
3127
3128    def in_sql(self, expression: exp.In) -> str:
3129        unnest = expression.args.get("unnest")
3130        if unnest:
3131            return self.sql(
3132                exp.replace_placeholders(
3133                    self.IN_UNNEST_TEMPLATE, arr=unnest.expressions[0], value=expression.this
3134                )
3135            )
3136        return super().in_sql(expression)
3137
3138    def join_sql(self, expression: exp.Join) -> str:
3139        if (
3140            not expression.args.get("using")
3141            and not expression.args.get("on")
3142            and not expression.method
3143            and (expression.kind in ("", "INNER", "OUTER"))
3144        ):
3145            # Some dialects support `LEFT/INNER JOIN UNNEST(...)` without an explicit ON clause
3146            # DuckDB doesn't, but we can just add a dummy ON clause that is always true
3147            if isinstance(expression.this, exp.Unnest):
3148                return super().join_sql(expression.on(exp.true()))
3149
3150            expression.set("side", None)
3151            expression.set("kind", None)
3152
3153        return super().join_sql(expression)
3154
3155    def countif_sql(self, expression: exp.CountIf) -> str:
3156        if self.dialect.version >= (1, 2):
3157            this = expression.this
3158            if expression.args.get("zero_on_all_null") and not isinstance(this, exp.Distinct):
3159                # DuckDB >= 1.2's COUNT_IF returns NULL when the condition is NULL on all rows,
3160                # so we wrap the condition in IS TRUE to preserve count-like semantics
3161                expression = exp.CountIf(this=exp.paren(this).is_(exp.true()))
3162            return self.function_fallback_sql(expression)
3163
3164        # https://github.com/tobymao/sqlglot/pull/4749
3165        return count_if_to_sum(self, expression)
3166
3167    def bracket_sql(self, expression: exp.Bracket) -> str:
3168        if self.dialect.version >= (1, 2):
3169            return super().bracket_sql(expression)
3170
3171        # https://duckdb.org/2025/02/05/announcing-duckdb-120.html#breaking-changes
3172        this = expression.this
3173        if isinstance(this, exp.Array):
3174            this.replace(exp.paren(this))
3175
3176        bracket = super().bracket_sql(expression)
3177
3178        if not expression.args.get("returns_list_for_maps"):
3179            if not this.type:
3180                from sqlglot.optimizer.annotate_types import annotate_types
3181
3182                this = annotate_types(this, dialect=self.dialect)
3183
3184            if this.is_type(exp.DType.MAP):
3185                bracket = f"({bracket})[1]"
3186
3187        return bracket
3188
3189    def withingroup_sql(self, expression: exp.WithinGroup) -> str:
3190        func = expression.this
3191
3192        # For ARRAY_AGG, DuckDB requires ORDER BY inside the function, not in WITHIN GROUP
3193        # Transform: ARRAY_AGG(x) WITHIN GROUP (ORDER BY y) -> ARRAY_AGG(x ORDER BY y)
3194        if isinstance(func, exp.ArrayAgg):
3195            if not isinstance(order := expression.expression, exp.Order):
3196                return self.sql(func)
3197
3198            # Save the original column for FILTER clause (before wrapping with Order)
3199            original_this = func.this
3200
3201            # Move ORDER BY inside ARRAY_AGG by wrapping its argument with Order
3202            # ArrayAgg.this should become Order(this=ArrayAgg.this, expressions=order.expressions)
3203            func.set(
3204                "this",
3205                exp.Order(
3206                    this=func.this.copy(),
3207                    expressions=order.expressions,
3208                ),
3209            )
3210
3211            # Generate the ARRAY_AGG function with ORDER BY and add FILTER clause if needed
3212            # Use original_this (not the Order-wrapped version) for the FILTER condition
3213            array_agg_sql = self.function_fallback_sql(func)
3214            return self._add_arrayagg_null_filter(array_agg_sql, func, original_this)
3215
3216        # For other functions (like PERCENTILES), use existing logic
3217        expression_sql = self.sql(expression, "expression")
3218
3219        if isinstance(func, exp.PERCENTILES):
3220            # Make the order key the first arg and slide the fraction to the right
3221            # https://duckdb.org/docs/sql/aggregates#ordered-set-aggregate-functions
3222            order_col = expression.find(exp.Ordered)
3223            if order_col:
3224                func.set("expression", func.this)
3225                func.set("this", order_col.this)
3226
3227        this = self.sql(expression, "this").rstrip(")")
3228
3229        return f"{this}{expression_sql})"
3230
3231    def length_sql(self, expression: exp.Length) -> str:
3232        arg = expression.this
3233
3234        # Dialects like BQ and Snowflake also accept binary values as args, so
3235        # DDB will attempt to infer the type or resort to case/when resolution
3236        if not expression.args.get("binary") or arg.is_string:
3237            return self.func("LENGTH", arg)
3238
3239        if not arg.type:
3240            from sqlglot.optimizer.annotate_types import annotate_types
3241
3242            arg = annotate_types(arg, dialect=self.dialect)
3243
3244        if arg.is_type(*exp.DataType.TEXT_TYPES):
3245            return self.func("LENGTH", arg)
3246
3247        # We need these casts to make duckdb's static type checker happy
3248        blob = exp.cast(arg, exp.DType.VARBINARY)
3249        varchar = exp.cast(arg, exp.DType.VARCHAR)
3250
3251        case = (
3252            exp.case(exp.Anonymous(this="TYPEOF", expressions=[arg]))
3253            .when(exp.Literal.string("BLOB"), exp.ByteLength(this=blob))
3254            .else_(exp.Anonymous(this="LENGTH", expressions=[varchar]))
3255        )
3256        return self.sql(case)
3257
3258    def bitlength_sql(self, expression: exp.BitLength) -> str:
3259        if not _is_binary(arg := expression.this):
3260            return self.func("BIT_LENGTH", arg)
3261
3262        blob = exp.cast(arg, exp.DataType.Type.VARBINARY)
3263        return self.sql(exp.ByteLength(this=blob) * exp.Literal.number(8))
3264
3265    def chr_sql(self, expression: exp.Chr, name: str = "CHR") -> str:
3266        arg = expression.expressions[0]
3267        if arg.is_type(*exp.DataType.REAL_TYPES):
3268            arg = exp.cast(arg, exp.DType.INT)
3269        return self.func("CHR", arg)
3270
3271    def collation_sql(self, expression: exp.Collation) -> str:
3272        self.unsupported("COLLATION function is not supported by DuckDB")
3273        return self.function_fallback_sql(expression)
3274
3275    def collate_sql(self, expression: exp.Collate) -> str:
3276        if not expression.expression.is_string:
3277            return super().collate_sql(expression)
3278
3279        raw = expression.expression.name
3280        if not raw:
3281            return self.sql(expression.this)
3282
3283        parts = []
3284        for part in raw.split("-"):
3285            lower = part.lower()
3286            if lower not in _SNOWFLAKE_COLLATION_DEFAULTS:
3287                if lower in _SNOWFLAKE_COLLATION_UNSUPPORTED:
3288                    self.unsupported(
3289                        f"Snowflake collation specifier '{part}' has no DuckDB equivalent"
3290                    )
3291                parts.append(lower)
3292
3293        if not parts:
3294            return self.sql(expression.this)
3295        return super().collate_sql(
3296            exp.Collate(this=expression.this, expression=exp.var(".".join(parts)))
3297        )
3298
3299    def _validate_regexp_flags(self, flags: exp.Expr | None, supported_flags: str) -> str | None:
3300        """
3301        Validate and filter regexp flags for DuckDB compatibility.
3302
3303        Args:
3304            flags: The flags expression to validate
3305            supported_flags: String of supported flags (e.g., "ims", "cims").
3306                            Only these flags will be returned.
3307
3308        Returns:
3309            Validated/filtered flag string, or None if no valid flags remain
3310        """
3311        if not isinstance(flags, exp.Expr):
3312            return None
3313
3314        if not flags.is_string:
3315            self.unsupported("Non-literal regexp flags are not fully supported in DuckDB")
3316            return None
3317
3318        flag_str = flags.this
3319        unsupported = set(flag_str) - set(supported_flags)
3320
3321        if unsupported:
3322            self.unsupported(
3323                f"Regexp flags {sorted(unsupported)} are not supported in this context"
3324            )
3325
3326        flag_str = "".join(f for f in flag_str if f in supported_flags)
3327        return flag_str if flag_str else None
3328
3329    def regexpcount_sql(self, expression: exp.RegexpCount) -> str:
3330        this = expression.this
3331        pattern = expression.expression
3332        position = expression.args.get("position")
3333        parameters = expression.args.get("parameters")
3334
3335        # Validate flags - only "ims" flags are supported for embedded patterns
3336        validated_flags = self._validate_regexp_flags(parameters, supported_flags="ims")
3337
3338        if position:
3339            this = exp.Substring(this=this, start=position)
3340
3341        # Embed flags in pattern (REGEXP_EXTRACT_ALL doesn't support flags argument)
3342        if validated_flags:
3343            pattern = exp.Concat(expressions=[exp.Literal.string(f"(?{validated_flags})"), pattern])
3344
3345        # Handle empty pattern: Snowflake returns 0, DuckDB would match between every character
3346        result = (
3347            exp.case()
3348            .when(
3349                exp.EQ(this=pattern, expression=exp.Literal.string("")),
3350                exp.Literal.number(0),
3351            )
3352            .else_(
3353                exp.Length(
3354                    this=exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern])
3355                )
3356            )
3357        )
3358
3359        return self.sql(result)
3360
3361    def regexpreplace_sql(self, expression: exp.RegexpReplace) -> str:
3362        subject = expression.this
3363        pattern = expression.expression
3364        replacement = expression.args.get("replacement") or exp.Literal.string("")
3365        position = expression.args.get("position")
3366        occurrence = expression.args.get("occurrence")
3367        modifiers = expression.args.get("modifiers")
3368
3369        validated_flags = self._validate_regexp_flags(modifiers, supported_flags="cimsg") or ""
3370
3371        # Handle occurrence (only literals supported)
3372        if occurrence and not occurrence.is_int:
3373            self.unsupported("REGEXP_REPLACE with non-literal occurrence")
3374        else:
3375            occurrence = occurrence.to_py() if occurrence and occurrence.is_int else 0
3376            if occurrence > 1:
3377                self.unsupported(f"REGEXP_REPLACE occurrence={occurrence} not supported")
3378            # flag duckdb to do either all or none, single_replace check is for duckdb round trip
3379            elif (
3380                occurrence == 0
3381                and "g" not in validated_flags
3382                and not expression.args.get("single_replace")
3383            ):
3384                validated_flags += "g"
3385
3386        # Handle position (only literals supported)
3387        prefix = None
3388        if position and not position.is_int:
3389            self.unsupported("REGEXP_REPLACE with non-literal position")
3390        elif position and position.is_int and position.to_py() > 1:
3391            pos = position.to_py()
3392            prefix = exp.Substring(
3393                this=subject, start=exp.Literal.number(1), length=exp.Literal.number(pos - 1)
3394            )
3395            subject = exp.Substring(this=subject, start=exp.Literal.number(pos))
3396
3397        result: exp.Expr = exp.Anonymous(
3398            this="REGEXP_REPLACE",
3399            expressions=[
3400                subject,
3401                pattern,
3402                replacement,
3403                exp.Literal.string(validated_flags) if validated_flags else None,
3404            ],
3405        )
3406
3407        if prefix:
3408            result = exp.Concat(expressions=[prefix, result])
3409
3410        return self.sql(result)
3411
3412    def regexplike_sql(self, expression: exp.RegexpLike) -> str:
3413        this = expression.this
3414        pattern = expression.expression
3415        flag = expression.args.get("flag")
3416
3417        if expression.args.get("full_match"):
3418            validated_flags = self._validate_regexp_flags(flag, supported_flags="cims")
3419            flag = exp.Literal.string(validated_flags) if validated_flags else None
3420            return self.func("REGEXP_FULL_MATCH", this, pattern, flag)
3421
3422        return self.func("REGEXP_MATCHES", this, pattern, flag)
3423
3424    @unsupported_args("ins_cost", "del_cost", "sub_cost")
3425    def levenshtein_sql(self, expression: exp.Levenshtein) -> str:
3426        this = expression.this
3427        expr = expression.expression
3428        max_dist = expression.args.get("max_dist")
3429
3430        if max_dist is None:
3431            return self.func("LEVENSHTEIN", this, expr)
3432
3433        # Emulate Snowflake semantics: if distance > max_dist, return max_dist
3434        levenshtein = exp.Levenshtein(this=this, expression=expr)
3435        return self.sql(exp.Least(this=levenshtein, expressions=[max_dist]))
3436
3437    def pad_sql(self, expression: exp.Pad) -> str:
3438        """
3439        Handle RPAD/LPAD for VARCHAR and BINARY types.
3440
3441        For VARCHAR: Delegate to parent class
3442        For BINARY: Lower to: input || REPEAT(pad, GREATEST(0, target_len - OCTET_LENGTH(input)))
3443        """
3444        string_arg = expression.this
3445        fill_arg = expression.args.get("fill_pattern") or exp.Literal.string(" ")
3446
3447        if _is_binary(string_arg) or _is_binary(fill_arg):
3448            length_arg = expression.expression
3449            is_left = expression.args.get("is_left")
3450
3451            input_len = exp.ByteLength(this=string_arg)
3452            chars_needed = length_arg - input_len
3453            pad_count = exp.Greatest(
3454                this=exp.Literal.number(0), expressions=[chars_needed], ignore_nulls=True
3455            )
3456            repeat_expr = exp.Repeat(this=fill_arg, times=pad_count)
3457
3458            left, right = string_arg, repeat_expr
3459            if is_left:
3460                left, right = right, left
3461
3462            result = exp.DPipe(this=left, expression=right)
3463            return self.sql(result)
3464
3465        # For VARCHAR: Delegate to parent class (handles PAD_FILL_PATTERN_IS_REQUIRED)
3466        return super().pad_sql(expression)
3467
3468    def minhash_sql(self, expression: exp.Minhash) -> str:
3469        k = expression.this
3470        exprs = expression.expressions
3471
3472        if len(exprs) != 1 or isinstance(exprs[0], exp.Star):
3473            self.unsupported(
3474                "MINHASH with multiple expressions or * requires manual query restructuring"
3475            )
3476            return self.func("MINHASH", k, *exprs)
3477
3478        expr = exprs[0]
3479        result = exp.replace_placeholders(self.MINHASH_TEMPLATE.copy(), expr=expr, k=k)
3480        return f"({self.sql(result)})"
3481
3482    def minhashcombine_sql(self, expression: exp.MinhashCombine) -> str:
3483        expr = expression.this
3484        result = exp.replace_placeholders(self.MINHASH_COMBINE_TEMPLATE.copy(), expr=expr)
3485        return f"({self.sql(result)})"
3486
3487    def approximatesimilarity_sql(self, expression: exp.ApproximateSimilarity) -> str:
3488        expr = expression.this
3489        result = exp.replace_placeholders(self.APPROXIMATE_SIMILARITY_TEMPLATE.copy(), expr=expr)
3490        return f"({self.sql(result)})"
3491
3492    def arrayuniqueagg_sql(self, expression: exp.ArrayUniqueAgg) -> str:
3493        return self.sql(
3494            exp.Filter(
3495                this=exp.func("LIST", exp.Distinct(expressions=[expression.this])),
3496                expression=exp.Where(this=expression.this.copy().is_(exp.null()).not_()),
3497            )
3498        )
3499
3500    def arrayconcatagg_sql(self, expression: exp.ArrayConcatAgg) -> str:
3501        this = expression.this
3502
3503        if isinstance(this, exp.Limit):
3504            self.unsupported("LIMIT in ARRAY_CONCAT_AGG cannot be transpiled to DuckDB")
3505            this = this.this
3506
3507        inner = this.this if isinstance(this, exp.Order) else this
3508
3509        return self.func(
3510            "FLATTEN",
3511            exp.Filter(
3512                this=exp.ArrayAgg(this=this),
3513                expression=exp.Where(this=inner.copy().is_(exp.null()).not_()),
3514            ),
3515        )
3516
3517    def arrayunionagg_sql(self, expression: exp.ArrayUnionAgg) -> str:
3518        self.unsupported("ARRAY_UNION_AGG is not supported in DuckDB")
3519        return self.function_fallback_sql(expression)
3520
3521    def arraydistinct_sql(self, expression: exp.ArrayDistinct) -> str:
3522        arr = expression.this
3523        func = self.func("LIST_DISTINCT", arr)
3524
3525        if expression.args.get("check_null"):
3526            add_null_to_array = exp.func(
3527                "LIST_APPEND", exp.func("LIST_DISTINCT", exp.ArrayCompact(this=arr)), exp.Null()
3528            )
3529            return self.sql(
3530                exp.If(
3531                    this=exp.NEQ(
3532                        this=exp.ArraySize(this=arr), expression=exp.func("LIST_COUNT", arr)
3533                    ),
3534                    true=add_null_to_array,
3535                    false=func,
3536                )
3537            )
3538
3539        return func
3540
3541    def arrayintersect_sql(self, expression: exp.ArrayIntersect) -> str:
3542        if expression.args.get("is_multiset") and len(expression.expressions) == 2:
3543            return self._array_bag_sql(
3544                self.ARRAY_INTERSECTION_CONDITION,
3545                expression.expressions[0],
3546                expression.expressions[1],
3547            )
3548        return self.function_fallback_sql(expression)
3549
3550    def arrayexcept_sql(self, expression: exp.ArrayExcept) -> str:
3551        arr1, arr2 = expression.this, expression.expression
3552        if expression.args.get("is_multiset"):
3553            return self._array_bag_sql(self.ARRAY_EXCEPT_CONDITION, arr1, arr2)
3554        return self.sql(
3555            exp.replace_placeholders(self.ARRAY_EXCEPT_SET_TEMPLATE, arr1=arr1, arr2=arr2)
3556        )
3557
3558    def arrayslice_sql(self, expression: exp.ArraySlice) -> str:
3559        """
3560        Transpiles Snowflake's ARRAY_SLICE (0-indexed, exclusive end) to DuckDB's
3561        ARRAY_SLICE (1-indexed, inclusive end) by wrapping start and end in CASE
3562        expressions that adjust the index at query time:
3563          - start: CASE WHEN start >= 0 THEN start + 1 ELSE start END
3564          - end:   CASE WHEN end < 0 THEN end - 1 ELSE end END
3565        """
3566        start, end = expression.args.get("start"), expression.args.get("end")
3567
3568        if expression.args.get("zero_based"):
3569            if start is not None:
3570                start = (
3571                    exp.case()
3572                    .when(
3573                        exp.GTE(this=start.copy(), expression=exp.Literal.number(0)),
3574                        exp.Add(this=start.copy(), expression=exp.Literal.number(1)),
3575                    )
3576                    .else_(start)
3577                )
3578            if end is not None:
3579                end = (
3580                    exp.case()
3581                    .when(
3582                        exp.LT(this=end.copy(), expression=exp.Literal.number(0)),
3583                        exp.Sub(this=end.copy(), expression=exp.Literal.number(1)),
3584                    )
3585                    .else_(end)
3586                )
3587
3588        return self.func("ARRAY_SLICE", expression.this, start, end, expression.args.get("step"))
3589
3590    def arrayszip_sql(self, expression: exp.ArraysZip) -> str:
3591        args = expression.expressions
3592
3593        if not args:
3594            # Return [{}] - using MAP([], []) since DuckDB can't represent empty structs
3595            return self.sql(exp.array(exp.Map(keys=exp.array(), values=exp.array())))
3596
3597        # Build placeholder values for template
3598        lengths = [exp.Length(this=arg) for arg in args]
3599        max_len = (
3600            lengths[0]
3601            if len(lengths) == 1
3602            else exp.Greatest(this=lengths[0], expressions=lengths[1:])
3603        )
3604
3605        # Empty struct with same schema: {'$1': NULL, '$2': NULL, ...}
3606        empty_struct = exp.func(
3607            "STRUCT",
3608            *[
3609                exp.PropertyEQ(this=exp.Literal.string(f"${i + 1}"), expression=exp.Null())
3610                for i in range(len(args))
3611            ],
3612        )
3613
3614        # Struct for transform: {'$1': COALESCE(arr1, [])[__i + 1], ...}
3615        # COALESCE wrapping handles NULL arrays - prevents invalid NULL[i] syntax
3616        index = exp.column("__i") + 1
3617        transform_struct = exp.func(
3618            "STRUCT",
3619            *[
3620                exp.PropertyEQ(
3621                    this=exp.Literal.string(f"${i + 1}"),
3622                    expression=exp.func("COALESCE", arg, exp.array())[index],
3623                )
3624                for i, arg in enumerate(args)
3625            ],
3626        )
3627
3628        result = exp.replace_placeholders(
3629            self.ARRAYS_ZIP_TEMPLATE.copy(),
3630            null_check=exp.or_(*[arg.is_(exp.Null()) for arg in args]),
3631            all_empty_check=exp.and_(
3632                *[
3633                    exp.EQ(this=exp.Length(this=arg), expression=exp.Literal.number(0))
3634                    for arg in args
3635                ]
3636            ),
3637            empty_struct=empty_struct,
3638            max_len=max_len,
3639            transform_struct=transform_struct,
3640        )
3641        return self.sql(result)
3642
3643    def lower_sql(self, expression: exp.Lower) -> str:
3644        result_sql = self.func("LOWER", _cast_to_varchar(expression.this))
3645        return _gen_with_cast_to_blob(self, expression, result_sql)
3646
3647    def upper_sql(self, expression: exp.Upper) -> str:
3648        result_sql = self.func("UPPER", _cast_to_varchar(expression.this))
3649        return _gen_with_cast_to_blob(self, expression, result_sql)
3650
3651    def reverse_sql(self, expression: exp.Reverse) -> str:
3652        result_sql = self.func("REVERSE", _cast_to_varchar(expression.this))
3653        return _gen_with_cast_to_blob(self, expression, result_sql)
3654
3655    def _left_right_sql(self, expression: exp.Left | exp.Right, func_name: str) -> str:
3656        arg = expression.this
3657        length = expression.expression
3658        is_binary = _is_binary(arg)
3659
3660        if is_binary:
3661            # LEFT/RIGHT(blob, n) becomes UNHEX(LEFT/RIGHT(HEX(blob), n * 2))
3662            # Each byte becomes 2 hex chars, so multiply length by 2
3663            hex_arg = exp.Hex(this=arg)
3664            hex_length = exp.Mul(this=length, expression=exp.Literal.number(2))
3665            result: exp.Expression = exp.Unhex(
3666                this=exp.Anonymous(this=func_name, expressions=[hex_arg, hex_length])
3667            )
3668        else:
3669            result = exp.Anonymous(this=func_name, expressions=[arg, length])
3670
3671        if expression.args.get("negative_length_returns_empty"):
3672            empty: exp.Expression = exp.Literal.string("")
3673            if is_binary:
3674                empty = exp.Unhex(this=empty)
3675            result = exp.case().when(length < exp.Literal.number(0), empty).else_(result)
3676
3677        return self.sql(result)
3678
3679    def left_sql(self, expression: exp.Left) -> str:
3680        return self._left_right_sql(expression, "LEFT")
3681
3682    def right_sql(self, expression: exp.Right) -> str:
3683        return self._left_right_sql(expression, "RIGHT")
3684
3685    def rtrimmedlength_sql(self, expression: exp.RtrimmedLength) -> str:
3686        return self.func("LENGTH", exp.Trim(this=expression.this, position="TRAILING"))
3687
3688    def stuff_sql(self, expression: exp.Stuff) -> str:
3689        base = expression.this
3690        start = expression.args["start"]
3691        length = expression.args["length"]
3692        insertion = expression.expression
3693        is_binary = _is_binary(base)
3694
3695        if is_binary:
3696            # DuckDB's SUBSTRING doesn't accept BLOB; operate on the HEX string instead
3697            # (each byte = 2 hex chars), then UNHEX back to BLOB
3698            base = exp.Hex(this=base)
3699            insertion = exp.Hex(this=insertion)
3700            left = exp.Substring(
3701                this=base.copy(),
3702                start=exp.Literal.number(1),
3703                length=(start.copy() - exp.Literal.number(1)) * exp.Literal.number(2),
3704            )
3705            right = exp.Substring(
3706                this=base.copy(),
3707                start=((start + length) - exp.Literal.number(1)) * exp.Literal.number(2)
3708                + exp.Literal.number(1),
3709            )
3710        else:
3711            left = exp.Substring(
3712                this=base.copy(),
3713                start=exp.Literal.number(1),
3714                length=start.copy() - exp.Literal.number(1),
3715            )
3716            right = exp.Substring(this=base.copy(), start=start + length)
3717        result: exp.Expr = exp.DPipe(
3718            this=exp.DPipe(this=left, expression=insertion), expression=right
3719        )
3720
3721        if is_binary:
3722            result = exp.Unhex(this=result)
3723
3724        return self.sql(result)
3725
3726    def rand_sql(self, expression: exp.Rand) -> str:
3727        seed = expression.this
3728        if seed is not None:
3729            self.unsupported("RANDOM with seed is not supported in DuckDB")
3730
3731        lower = expression.args.get("lower")
3732        upper = expression.args.get("upper")
3733
3734        if lower and upper:
3735            # scale DuckDB's [0,1) to the specified range
3736            range_size = exp.paren(upper - lower)
3737            scaled = exp.Add(this=lower, expression=exp.func("random") * range_size)
3738
3739            # For now we assume that if bounds are set, return type is BIGINT. Snowflake/Teradata
3740            result = exp.cast(scaled, exp.DType.BIGINT)
3741            return self.sql(result)
3742
3743        # Default DuckDB behavior - just return RANDOM() as float
3744        return "RANDOM()"
3745
3746    def bytelength_sql(self, expression: exp.ByteLength) -> str:
3747        arg = expression.this
3748
3749        # Check if it's a text type (handles both literals and annotated expressions)
3750        if arg.is_type(*exp.DataType.TEXT_TYPES):
3751            return self.func("OCTET_LENGTH", exp.Encode(this=arg))
3752
3753        # Default: pass through as-is (conservative for DuckDB, handles binary and unannotated)
3754        return self.func("OCTET_LENGTH", arg)
3755
3756    def base64encode_sql(self, expression: exp.Base64Encode) -> str:
3757        # DuckDB TO_BASE64 requires BLOB input
3758        # Snowflake BASE64_ENCODE accepts both VARCHAR and BINARY - for VARCHAR it implicitly
3759        # encodes UTF-8 bytes. We add ENCODE unless the input is a binary type.
3760        result = expression.this
3761
3762        # Check if input is a string type - ENCODE only accepts VARCHAR
3763        if result.is_type(*exp.DataType.TEXT_TYPES):
3764            result = exp.Encode(this=result)
3765
3766        result = exp.ToBase64(this=result)
3767
3768        max_line_length = expression.args.get("max_line_length")
3769        alphabet = expression.args.get("alphabet")
3770
3771        # Handle custom alphabet by replacing standard chars with custom ones
3772        result = _apply_base64_alphabet_replacements(result, alphabet)
3773
3774        # Handle max_line_length by inserting newlines every N characters
3775        line_length = (
3776            t.cast(int, max_line_length.to_py())
3777            if isinstance(max_line_length, exp.Literal) and max_line_length.is_number
3778            else 0
3779        )
3780        if line_length > 0:
3781            newline = exp.Chr(expressions=[exp.Literal.number(10)])
3782            result = exp.Trim(
3783                this=exp.RegexpReplace(
3784                    this=result,
3785                    expression=exp.Literal.string(f"(.{{{line_length}}})"),
3786                    replacement=exp.Concat(expressions=[exp.Literal.string("\\1"), newline.copy()]),
3787                ),
3788                expression=newline,
3789                position="TRAILING",
3790            )
3791
3792        return self.sql(result)
3793
3794    def hex_sql(self, expression: exp.Hex) -> str:
3795        case = expression.args.get("case")
3796
3797        if not case:
3798            return self.func("HEX", expression.this)
3799
3800        hex_expr = exp.Hex(this=expression.this)
3801        return self.sql(
3802            exp.case()
3803            .when(case.is_(exp.null()), exp.null())
3804            .when(case.copy().eq(0), exp.Lower(this=hex_expr.copy()))
3805            .else_(hex_expr)
3806        )
3807
3808    def replace_sql(self, expression: exp.Replace) -> str:
3809        result_sql = self.func(
3810            "REPLACE",
3811            _cast_to_varchar(expression.this),
3812            _cast_to_varchar(expression.expression),
3813            _cast_to_varchar(expression.args.get("replacement")),
3814        )
3815        return _gen_with_cast_to_blob(self, expression, result_sql)
3816
3817    def _bitwise_op(self, expression: exp.Binary, op: str) -> str:
3818        _prepare_binary_bitwise_args(expression)
3819        result_sql = self.binary(expression, op)
3820        return _gen_with_cast_to_blob(self, expression, result_sql)
3821
3822    def bitwisexor_sql(self, expression: exp.BitwiseXor) -> str:
3823        _prepare_binary_bitwise_args(expression)
3824        result_sql = self.func("XOR", expression.this, expression.expression)
3825        return _gen_with_cast_to_blob(self, expression, result_sql)
3826
3827    def objectinsert_sql(self, expression: exp.ObjectInsert) -> str:
3828        this = expression.this
3829        key = expression.args.get("key")
3830        key_sql = key.name if isinstance(key, exp.Expr) else ""
3831        value_sql = self.sql(expression, "value")
3832
3833        kv_sql = f"{key_sql} := {value_sql}"
3834
3835        # If the input struct is empty e.g. transpiling OBJECT_INSERT(OBJECT_CONSTRUCT(), key, value) from Snowflake
3836        # then we can generate STRUCT_PACK which will build it since STRUCT_INSERT({}, key := value) is not valid DuckDB
3837        if isinstance(this, exp.Struct) and not this.expressions:
3838            return self.func("STRUCT_PACK", kv_sql)
3839
3840        return self.func("STRUCT_INSERT", this, kv_sql)
3841
3842    def mapcat_sql(self, expression: exp.MapCat) -> str:
3843        result = exp.replace_placeholders(
3844            self.MAPCAT_TEMPLATE.copy(),
3845            map1=expression.this,
3846            map2=expression.expression,
3847        )
3848        return self.sql(result)
3849
3850    def mapcontainskey_sql(self, expression: exp.MapContainsKey) -> str:
3851        return self.func(
3852            "ARRAY_CONTAINS", exp.func("MAP_KEYS", expression.args["key"]), expression.this
3853        )
3854
3855    def mapdelete_sql(self, expression: exp.MapDelete) -> str:
3856        map_arg = expression.this
3857        keys_to_delete = expression.expressions
3858
3859        x_dot_key = exp.Dot(this=exp.to_identifier("x"), expression=exp.to_identifier("key"))
3860
3861        lambda_expr = exp.Lambda(
3862            this=exp.In(this=x_dot_key, expressions=keys_to_delete).not_(),
3863            expressions=[exp.to_identifier("x")],
3864        )
3865        result = exp.func(
3866            "MAP_FROM_ENTRIES",
3867            exp.ArrayFilter(this=exp.func("MAP_ENTRIES", map_arg), expression=lambda_expr),
3868        )
3869        return self.sql(result)
3870
3871    def mappick_sql(self, expression: exp.MapPick) -> str:
3872        map_arg = expression.this
3873        keys_to_pick = expression.expressions
3874
3875        x_dot_key = exp.Dot(this=exp.to_identifier("x"), expression=exp.to_identifier("key"))
3876
3877        if len(keys_to_pick) == 1 and keys_to_pick[0].is_type(exp.DType.ARRAY):
3878            lambda_expr = exp.Lambda(
3879                this=exp.func("ARRAY_CONTAINS", keys_to_pick[0], x_dot_key),
3880                expressions=[exp.to_identifier("x")],
3881            )
3882        else:
3883            lambda_expr = exp.Lambda(
3884                this=exp.In(this=x_dot_key, expressions=keys_to_pick),
3885                expressions=[exp.to_identifier("x")],
3886            )
3887
3888        result = exp.func(
3889            "MAP_FROM_ENTRIES",
3890            exp.func("LIST_FILTER", exp.func("MAP_ENTRIES", map_arg), lambda_expr),
3891        )
3892        return self.sql(result)
3893
3894    def mapsize_sql(self, expression: exp.MapSize) -> str:
3895        return self.func("CARDINALITY", expression.this)
3896
3897    @unsupported_args("update_flag")
3898    def mapinsert_sql(self, expression: exp.MapInsert) -> str:
3899        map_arg = expression.this
3900        key = expression.args.get("key")
3901        value = expression.args.get("value")
3902
3903        map_type = map_arg.type
3904
3905        if value is not None:
3906            if map_type and map_type.expressions and len(map_type.expressions) > 1:
3907                # Extract the value type from MAP(key_type, value_type)
3908                value_type = map_type.expressions[1]
3909                # Cast value to match the map's value type to avoid type conflicts
3910                value = exp.cast(value, value_type)
3911            # else: polymorphic MAP case - no type parameters available, use value as-is
3912
3913        # Create a single-entry map for the new key-value pair
3914        new_entry_struct = exp.Struct(expressions=[exp.PropertyEQ(this=key, expression=value)])
3915        new_entry: exp.Expression = exp.ToMap(this=new_entry_struct)
3916
3917        # Use MAP_CONCAT to merge the original map with the new entry
3918        # This automatically handles both insert and update cases
3919        result = exp.func("MAP_CONCAT", map_arg, new_entry)
3920
3921        return self.sql(result)
3922
3923    def startswith_sql(self, expression: exp.StartsWith) -> str:
3924        return self.func(
3925            "STARTS_WITH",
3926            _cast_to_varchar(expression.this),
3927            _cast_to_varchar(expression.expression),
3928        )
3929
3930    def space_sql(self, expression: exp.Space) -> str:
3931        # DuckDB's REPEAT requires BIGINT for the count parameter
3932        return self.sql(
3933            exp.Repeat(
3934                this=exp.Literal.string(" "),
3935                times=exp.cast(expression.this, exp.DType.BIGINT),
3936            )
3937        )
3938
3939    def tablefromrows_sql(self, expression: exp.TableFromRows) -> str:
3940        # For GENERATOR, unwrap TABLE() - just emit the Generator (becomes RANGE)
3941        if isinstance(expression.this, exp.Generator):
3942            # Preserve alias, joins, and other table-level args
3943            table = exp.Table(
3944                this=expression.this,
3945                alias=expression.args.get("alias"),
3946                joins=expression.args.get("joins"),
3947            )
3948            return self.sql(table)
3949
3950        return super().tablefromrows_sql(expression)
3951
3952    def unnest_sql(self, expression: exp.Unnest) -> str:
3953        explode_array = expression.args.get("explode_array")
3954        if explode_array:
3955            # In BigQuery, UNNESTing a nested array leads to explosion of the top-level array & struct
3956            # This is transpiled to DDB by transforming "FROM UNNEST(...)" to "FROM (SELECT UNNEST(..., max_depth => 2))"
3957            expression.expressions.append(
3958                exp.Kwarg(this=exp.var("max_depth"), expression=exp.Literal.number(2))
3959            )
3960
3961            # If BQ's UNNEST is aliased, we transform it from a column alias to a table alias in DDB
3962            alias = expression.args.get("alias")
3963            if isinstance(alias, exp.TableAlias):
3964                expression.set("alias", None)
3965                if alias.columns:
3966                    alias = exp.TableAlias(this=seq_get(alias.columns, 0))
3967
3968            unnest_sql = super().unnest_sql(expression)
3969            select = exp.Select(expressions=[unnest_sql]).subquery(alias)
3970            return self.sql(select)
3971
3972        return super().unnest_sql(expression)
3973
3974    def arrayagg_sql(self, expression: exp.ArrayAgg) -> str:
3975        if isinstance(expression.this, exp.Limit):
3976            self.unsupported("LIMIT inside ARRAY_AGG is not supported in DuckDB")
3977
3978        return super().arrayagg_sql(expression)
3979
3980    def ignorenulls_sql(self, expression: exp.IgnoreNulls) -> str:
3981        this = expression.this
3982
3983        if isinstance(this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
3984            # DuckDB should render IGNORE NULLS only for the general-purpose
3985            # window functions that accept it e.g. FIRST_VALUE(... IGNORE NULLS) OVER (...)
3986            return super().ignorenulls_sql(expression)
3987
3988        # For ARRAY_AGG(expr IGNORE NULLS ...), convert IGNORE NULLS to a
3989        # FILTER(WHERE expr IS NOT NULL) clause by setting nulls_excluded on
3990        # the ArrayAgg.  The existing _add_arrayagg_null_filter method will
3991        # emit the FILTER clause during arrayagg_sql / withingroup_sql.
3992        if isinstance(this, exp.ArrayAgg):
3993            this.set("nulls_excluded", True)
3994            return self.sql(this)
3995
3996        if isinstance(this, exp.First):
3997            this = exp.AnyValue(this=this.this)
3998
3999        if not isinstance(this, (exp.AnyValue, exp.ApproxQuantiles)):
4000            self.unsupported("IGNORE NULLS is not supported for non-window functions.")
4001
4002        return self.sql(this)
4003
4004    def split_sql(self, expression: exp.Split) -> str:
4005        base_func = exp.func("STR_SPLIT", expression.this, expression.expression)
4006
4007        case_expr = exp.case().else_(base_func)
4008        needs_case = False
4009
4010        if expression.args.get("null_returns_null"):
4011            case_expr = case_expr.when(expression.expression.is_(exp.null()), exp.null())
4012            needs_case = True
4013
4014        if expression.args.get("empty_delimiter_returns_whole"):
4015            # When delimiter is empty string, return input string as single array element
4016            array_with_input = exp.array(expression.this)
4017            case_expr = case_expr.when(
4018                expression.expression.eq(exp.Literal.string("")), array_with_input
4019            )
4020            needs_case = True
4021
4022        return self.sql(case_expr if needs_case else base_func)
4023
4024    def splitpart_sql(self, expression: exp.SplitPart) -> str:
4025        string_arg = expression.this
4026        delimiter_arg = expression.args.get("delimiter")
4027        part_index_arg = expression.args.get("part_index")
4028
4029        if delimiter_arg and part_index_arg:
4030            # Handle Snowflake's "index 0 and 1 both return first element" behavior
4031            if expression.args.get("part_index_zero_as_one"):
4032                # Convert 0 to 1 for compatibility
4033
4034                part_index_arg = exp.Paren(
4035                    this=exp.case()
4036                    .when(part_index_arg.eq(exp.Literal.number("0")), exp.Literal.number("1"))
4037                    .else_(part_index_arg)
4038                )
4039
4040            # Use Anonymous to avoid recursion
4041            base_func_expr: exp.Expr = exp.Anonymous(
4042                this="SPLIT_PART", expressions=[string_arg, delimiter_arg, part_index_arg]
4043            )
4044            needs_case_transform = False
4045            case_expr = exp.case().else_(base_func_expr)
4046
4047            if expression.args.get("empty_delimiter_returns_whole"):
4048                # When delimiter is empty string:
4049                # - Return whole string if part_index is 1 or -1
4050                # - Return empty string otherwise
4051                empty_case = exp.Paren(
4052                    this=exp.case()
4053                    .when(
4054                        exp.or_(
4055                            part_index_arg.eq(exp.Literal.number("1")),
4056                            part_index_arg.eq(exp.Literal.number("-1")),
4057                        ),
4058                        string_arg,
4059                    )
4060                    .else_(exp.Literal.string(""))
4061                )
4062
4063                case_expr = case_expr.when(delimiter_arg.eq(exp.Literal.string("")), empty_case)
4064                needs_case_transform = True
4065
4066            """
4067            Output looks something like this:
4068
4069            CASE
4070            WHEN delimiter is '' THEN
4071                (
4072                    CASE
4073                    WHEN adjusted_part_index = 1 OR adjusted_part_index = -1 THEN input
4074                    ELSE '' END
4075                )
4076            ELSE SPLIT_PART(input, delimiter, adjusted_part_index)
4077            END
4078
4079            """
4080            return self.sql(case_expr if needs_case_transform else base_func_expr)
4081
4082        return self.function_fallback_sql(expression)
4083
4084    def respectnulls_sql(self, expression: exp.RespectNulls) -> str:
4085        if isinstance(expression.this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
4086            # DuckDB should render RESPECT NULLS only for the general-purpose
4087            # window functions that accept it e.g. FIRST_VALUE(... RESPECT NULLS) OVER (...)
4088            return super().respectnulls_sql(expression)
4089
4090        self.unsupported("RESPECT NULLS is not supported for non-window functions.")
4091        return self.sql(expression, "this")
4092
4093    def arraytostring_sql(self, expression: exp.ArrayToString) -> str:
4094        null = expression.args.get("null")
4095
4096        if expression.args.get("null_is_empty"):
4097            x = exp.to_identifier("x")
4098            list_transform = exp.Transform(
4099                this=expression.this.copy(),
4100                expression=exp.Lambda(
4101                    this=exp.Coalesce(
4102                        this=exp.cast(x, "TEXT"), expressions=[exp.Literal.string("")]
4103                    ),
4104                    expressions=[x],
4105                ),
4106            )
4107            array_to_string = exp.ArrayToString(
4108                this=list_transform, expression=expression.expression
4109            )
4110            if expression.args.get("null_delim_is_null"):
4111                return self.sql(
4112                    exp.case()
4113                    .when(expression.expression.copy().is_(exp.null()), exp.null())
4114                    .else_(array_to_string)
4115                )
4116            return self.sql(array_to_string)
4117
4118        if null:
4119            x = exp.to_identifier("x")
4120            return self.sql(
4121                exp.ArrayToString(
4122                    this=exp.Transform(
4123                        this=expression.this,
4124                        expression=exp.Lambda(
4125                            this=exp.Coalesce(this=x, expressions=[null]),
4126                            expressions=[x],
4127                        ),
4128                    ),
4129                    expression=expression.expression,
4130                )
4131            )
4132
4133        return self.func("ARRAY_TO_STRING", expression.this, expression.expression)
4134
4135    def concatws_sql(self, expression: exp.ConcatWs) -> str:
4136        # DuckDB-specific: handle binary types using DPipe (||) operator
4137        separator = seq_get(expression.expressions, 0)
4138        args = expression.expressions[1:]
4139
4140        if any(_is_binary(arg) for arg in [separator, *args]):
4141            result = args[0]
4142            for arg in args[1:]:
4143                result = exp.DPipe(
4144                    this=exp.DPipe(this=result, expression=separator), expression=arg
4145                )
4146            return self.sql(result)
4147
4148        return super().concatws_sql(expression)
4149
4150    def _regexp_extract_sql(self, expression: exp.RegexpExtract | exp.RegexpExtractAll) -> str:
4151        this = expression.this
4152        group = expression.args.get("group")
4153        params = expression.args.get("parameters")
4154        position = expression.args.get("position")
4155        occurrence = expression.args.get("occurrence")
4156        null_if_pos_overflow = expression.args.get("null_if_pos_overflow")
4157
4158        # Handle Snowflake's 'e' flag: it enables capture group extraction
4159        # In DuckDB, this is controlled by the group parameter directly
4160        if params and params.is_string and "e" in params.name:
4161            params = exp.Literal.string(params.name.replace("e", ""))
4162
4163        validated_flags = self._validate_regexp_flags(params, supported_flags="cims")
4164
4165        # Strip default group when no following params (DuckDB default is same as group=0)
4166        if (
4167            not validated_flags
4168            and group
4169            and group.name == str(self.dialect.REGEXP_EXTRACT_DEFAULT_GROUP)
4170        ):
4171            group = None
4172
4173        flags_expr = exp.Literal.string(validated_flags) if validated_flags else None
4174
4175        # use substring to handle position argument
4176        if position and (not position.is_int or position.to_py() > 1):
4177            this = exp.Substring(this=this, start=position)
4178
4179            if null_if_pos_overflow:
4180                this = exp.Nullif(this=this, expression=exp.Literal.string(""))
4181
4182        is_extract_all = isinstance(expression, exp.RegexpExtractAll)
4183        non_single_occurrence = occurrence and (not occurrence.is_int or occurrence.to_py() > 1)
4184
4185        if is_extract_all or non_single_occurrence:
4186            name = "REGEXP_EXTRACT_ALL"
4187        else:
4188            name = "REGEXP_EXTRACT"
4189
4190        result: exp.Expr = exp.Anonymous(
4191            this=name, expressions=[this, expression.expression, group, flags_expr]
4192        )
4193
4194        # Array slicing for REGEXP_EXTRACT_ALL with occurrence
4195        if is_extract_all and non_single_occurrence:
4196            result = exp.Bracket(this=result, expressions=[exp.Slice(this=occurrence)])
4197        # ARRAY_EXTRACT for REGEXP_EXTRACT with occurrence > 1
4198        elif non_single_occurrence:
4199            result = exp.Anonymous(this="ARRAY_EXTRACT", expressions=[result, occurrence])
4200
4201        return self.sql(result)
4202
4203    def regexpextract_sql(self, expression: exp.RegexpExtract) -> str:
4204        return self._regexp_extract_sql(expression)
4205
4206    def regexpextractall_sql(self, expression: exp.RegexpExtractAll) -> str:
4207        return self._regexp_extract_sql(expression)
4208
4209    def regexpinstr_sql(self, expression: exp.RegexpInstr) -> str:
4210        this = expression.this
4211        pattern = expression.expression
4212        position = expression.args.get("position")
4213        orig_occ = expression.args.get("occurrence")
4214        occurrence = orig_occ or exp.Literal.number(1)
4215        option = expression.args.get("option")
4216        parameters = expression.args.get("parameters")
4217
4218        validated_flags = self._validate_regexp_flags(parameters, supported_flags="ims")
4219        if validated_flags:
4220            pattern = exp.Concat(expressions=[exp.Literal.string(f"(?{validated_flags})"), pattern])
4221
4222        # Handle starting position offset
4223        pos_offset: exp.Expr = exp.Literal.number(0)
4224        if position and (not position.is_int or position.to_py() > 1):
4225            this = exp.Substring(this=this, start=position)
4226            pos_offset = position - exp.Literal.number(1)
4227
4228        # Helper: LIST_SUM(LIST_TRANSFORM(list[1:end], x -> LENGTH(x)))
4229        def sum_lengths(func_name: str, end: exp.Expr) -> exp.Expr:
4230            lst = exp.Bracket(
4231                this=exp.Anonymous(this=func_name, expressions=[this, pattern]),
4232                expressions=[exp.Slice(this=exp.Literal.number(1), expression=end)],
4233                offset=1,
4234            )
4235            transform = exp.Anonymous(
4236                this="LIST_TRANSFORM",
4237                expressions=[
4238                    lst,
4239                    exp.Lambda(
4240                        this=exp.Length(this=exp.to_identifier("x")),
4241                        expressions=[exp.to_identifier("x")],
4242                    ),
4243                ],
4244            )
4245            return exp.Coalesce(
4246                this=exp.Anonymous(this="LIST_SUM", expressions=[transform]),
4247                expressions=[exp.Literal.number(0)],
4248            )
4249
4250        # Position = 1 + sum(split_lengths[1:occ]) + sum(match_lengths[1:occ-1]) + offset
4251        base_pos: exp.Expr = (
4252            exp.Literal.number(1)
4253            + sum_lengths("STRING_SPLIT_REGEX", occurrence)
4254            + sum_lengths("REGEXP_EXTRACT_ALL", occurrence - exp.Literal.number(1))
4255            + pos_offset
4256        )
4257
4258        # option=1: add match length for end position
4259        if option and option.is_int and option.to_py() == 1:
4260            match_at_occ = exp.Bracket(
4261                this=exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern]),
4262                expressions=[occurrence],
4263                offset=1,
4264            )
4265            base_pos = base_pos + exp.Coalesce(
4266                this=exp.Length(this=match_at_occ), expressions=[exp.Literal.number(0)]
4267            )
4268
4269        # NULL checks for all provided arguments
4270        # .copy() is used strictly because .is_() alters the node's parent pointer, mutating the parsed AST
4271        null_args = [
4272            expression.this,
4273            expression.expression,
4274            position,
4275            orig_occ,
4276            option,
4277            parameters,
4278        ]
4279        null_checks = [arg.copy().is_(exp.Null()) for arg in null_args if arg]
4280
4281        matches = exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern])
4282
4283        return self.sql(
4284            exp.case()
4285            .when(exp.or_(*null_checks), exp.Null())
4286            .when(pattern.copy().eq(exp.Literal.string("")), exp.Literal.number(0))
4287            .when(exp.Length(this=matches) < occurrence, exp.Literal.number(0))
4288            .else_(base_pos)
4289        )
4290
4291    @unsupported_args("culture")
4292    def numbertostr_sql(self, expression: exp.NumberToStr) -> str:
4293        fmt = expression.args.get("format")
4294        if fmt and fmt.is_int:
4295            return self.func("FORMAT", f"'{{:,.{fmt.name}f}}'", expression.this)
4296
4297        self.unsupported("Only integer formats are supported by NumberToStr")
4298        return self.function_fallback_sql(expression)
4299
4300    def autoincrementcolumnconstraint_sql(self, _) -> str:
4301        self.unsupported("The AUTOINCREMENT column constraint is not supported by DuckDB")
4302        return ""
4303
4304    def aliases_sql(self, expression: exp.Aliases) -> str:
4305        this = expression.this
4306        if isinstance(this, exp.Posexplode):
4307            return self.posexplode_sql(this)
4308
4309        return super().aliases_sql(expression)
4310
4311    def posexplode_sql(self, expression: exp.Posexplode) -> str:
4312        this = expression.this
4313        parent = expression.parent
4314
4315        # The default Spark aliases are "pos" and "col", unless specified otherwise
4316        pos, col = exp.to_identifier("pos"), exp.to_identifier("col")
4317
4318        if isinstance(parent, exp.Aliases):
4319            # Column case: SELECT POSEXPLODE(col) [AS (a, b)]
4320            pos, col = parent.expressions
4321        elif isinstance(parent, exp.Table):
4322            # Table case: SELECT * FROM POSEXPLODE(col) [AS (a, b)]
4323            alias = parent.args.get("alias")
4324            if alias:
4325                pos, col = alias.columns or [pos, col]
4326                alias.pop()
4327
4328        # Translate POSEXPLODE to UNNEST + GENERATE_SUBSCRIPTS
4329        # Note: In Spark pos is 0-indexed, but in DuckDB it's 1-indexed, so we subtract 1 from GENERATE_SUBSCRIPTS
4330        unnest_sql = self.sql(exp.Unnest(expressions=[this], alias=col))
4331        gen_subscripts = self.sql(
4332            exp.Alias(
4333                this=exp.Anonymous(
4334                    this="GENERATE_SUBSCRIPTS", expressions=[this, exp.Literal.number(1)]
4335                )
4336                - exp.Literal.number(1),
4337                alias=pos,
4338            )
4339        )
4340
4341        posexplode_sql = self.format_args(gen_subscripts, unnest_sql)
4342
4343        if isinstance(parent, exp.From) or (parent and isinstance(parent.parent, exp.From)):
4344            # SELECT * FROM POSEXPLODE(col) -> SELECT * FROM (SELECT GENERATE_SUBSCRIPTS(...), UNNEST(...))
4345            return self.sql(exp.Subquery(this=exp.Select(expressions=[posexplode_sql])))
4346
4347        return posexplode_sql
4348
4349    def addmonths_sql(self, expression: exp.AddMonths) -> str:
4350        """
4351        Handles three key issues:
4352        1. Float/decimal months: e.g., Snowflake rounds, whereas DuckDB INTERVAL requires integers
4353        2. End-of-month preservation: If input is last day of month, result is last day of result month
4354        3. Type preservation: Maintains DATE/TIMESTAMPTZ types (DuckDB defaults to TIMESTAMP)
4355        """
4356        from sqlglot.optimizer.annotate_types import annotate_types
4357
4358        this = expression.this
4359        if not this.type:
4360            this = annotate_types(this, dialect=self.dialect)
4361
4362        if this.is_type(*exp.DataType.TEXT_TYPES):
4363            this = exp.Cast(this=this, to=exp.DataType(this=exp.DType.TIMESTAMP))
4364
4365        # Detect float/decimal months to apply rounding (Snowflake behavior)
4366        # DuckDB INTERVAL syntax doesn't support non-integer expressions, so use TO_MONTHS
4367        months_expr = expression.expression
4368        if not months_expr.type:
4369            months_expr = annotate_types(months_expr, dialect=self.dialect)
4370
4371        # Build interval or to_months expression based on type
4372        # Float/decimal case: Round and use TO_MONTHS(CAST(ROUND(value) AS INT))
4373        interval_or_to_months = (
4374            exp.func("TO_MONTHS", exp.cast(exp.func("ROUND", months_expr), "INT"))
4375            if months_expr.is_type(
4376                exp.DType.FLOAT,
4377                exp.DType.DOUBLE,
4378                exp.DType.DECIMAL,
4379            )
4380            # Integer case: standard INTERVAL N MONTH syntax
4381            else exp.Interval(this=months_expr, unit=exp.var("MONTH"))
4382        )
4383
4384        date_add_expr = exp.Add(this=this, expression=interval_or_to_months)
4385
4386        # Apply end-of-month preservation if Snowflake flag is set
4387        # CASE WHEN LAST_DAY(date) = date THEN LAST_DAY(result) ELSE result END
4388        preserve_eom = expression.args.get("preserve_end_of_month")
4389        result_expr = (
4390            exp.case()
4391            .when(
4392                exp.EQ(this=exp.func("LAST_DAY", this), expression=this),
4393                exp.func("LAST_DAY", date_add_expr),
4394            )
4395            .else_(date_add_expr)
4396            if preserve_eom
4397            else date_add_expr
4398        )
4399
4400        # DuckDB's DATE_ADD function returns TIMESTAMP/DATETIME by default, even when the input is DATE
4401        # To match for example Snowflake's ADD_MONTHS behavior (which preserves the input type)
4402        # We need to cast the result back to the original type when the input is DATE or TIMESTAMPTZ
4403        # Example: ADD_MONTHS('2023-01-31'::date, 1) should return DATE, not TIMESTAMP
4404        if this.is_type(exp.DType.DATE, exp.DType.TIMESTAMPTZ):
4405            return self.sql(exp.Cast(this=result_expr, to=this.type))
4406        return self.sql(result_expr)
4407
4408    def format_sql(self, expression: exp.Format) -> str:
4409        if expression.name.lower() == "%s" and len(expression.expressions) == 1:
4410            return self.func("FORMAT", "'{}'", expression.expressions[0])
4411
4412        return self.function_fallback_sql(expression)
4413
4414    def hexstring_sql(
4415        self, expression: exp.HexString, binary_function_repr: str | None = None
4416    ) -> str:
4417        # UNHEX('FF') correctly produces blob \xFF in DuckDB
4418        return super().hexstring_sql(expression, binary_function_repr="UNHEX")
4419
4420    def datetrunc_sql(self, expression: exp.DateTrunc) -> str:
4421        unit = expression.args.get("unit")
4422        date = expression.this
4423
4424        week_start = _week_trunc_start_dow(unit)
4425        unit = unit_to_str(expression)
4426
4427        if week_start:
4428            result = self.sql(
4429                _build_week_trunc_expression(date, week_start, preserve_start_day=True)
4430            )
4431        else:
4432            result = self.func("DATE_TRUNC", unit, date)
4433
4434        if (
4435            expression.args.get("input_type_preserved")
4436            and date.is_type(*exp.DataType.TEMPORAL_TYPES)
4437            and not (is_date_unit(unit) and date.is_type(exp.DType.DATE))
4438        ):
4439            return self.sql(exp.Cast(this=result, to=date.type))
4440
4441        return result
4442
4443    def datetimetrunc_sql(self, expression: exp.DatetimeTrunc) -> str:
4444        this = exp.cast(expression.this, exp.DType.DATETIME)
4445        week_start = _week_trunc_start_dow(expression.args.get("unit"))
4446        if week_start:
4447            return self.sql(
4448                _build_week_trunc_expression(
4449                    this, week_start, preserve_start_day=True, cast_to_date=False
4450                )
4451            )
4452
4453        return self.func("DATE_TRUNC", unit_to_str(expression), this)
4454
4455    def timestamptrunc_sql(self, expression: exp.TimestampTrunc) -> str:
4456        zone = expression.args.get("zone")
4457        timestamp = expression.this
4458        week_start = _week_trunc_start_dow(expression.args.get("unit"))
4459
4460        # The week start emulation below is exact, so avoid weekstart_unit_to_str's degrade warning
4461        unit = unit_to_str(expression) if week_start else weekstart_unit_to_str(self, expression)
4462        date_unit = is_date_unit(unit) or bool(week_start)
4463
4464        def _trunc_expr(this: exp.Expr) -> exp.Expr:
4465            if week_start:
4466                return _build_week_trunc_expression(
4467                    this, week_start, preserve_start_day=True, cast_to_date=False
4468                )
4469            return exp.func("DATE_TRUNC", unit, this)
4470
4471        if date_unit and zone:
4472            # BigQuery's TIMESTAMP_TRUNC with timezone truncates in the target timezone and returns as UTC.
4473            # Double AT TIME ZONE needed for BigQuery compatibility:
4474            # 1. First AT TIME ZONE: ensures truncation happens in the target timezone
4475            # 2. Second AT TIME ZONE: converts the DATE result back to TIMESTAMPTZ (preserving time component)
4476            timestamp = exp.AtTimeZone(this=timestamp, zone=zone)
4477            trunced = _trunc_expr(timestamp)
4478            if isinstance(trunced, exp.DateAdd):
4479                # Parenthesize so the trailing AT TIME ZONE binds to the whole shifted expression
4480                trunced = exp.Paren(this=trunced)
4481            return self.sql(exp.AtTimeZone(this=trunced, zone=zone))
4482
4483        result = self.sql(_trunc_expr(timestamp))
4484        if expression.args.get("input_type_preserved"):
4485            if timestamp.type and timestamp.is_type(exp.DType.TIME, exp.DType.TIMETZ):
4486                dummy_date = exp.Cast(
4487                    this=exp.Literal.string("1970-01-01"),
4488                    to=exp.DataType(this=exp.DType.DATE),
4489                )
4490                date_time = exp.Add(this=dummy_date, expression=timestamp)
4491                result = self.func("DATE_TRUNC", unit, date_time)
4492                return self.sql(exp.Cast(this=result, to=timestamp.type))
4493
4494            if timestamp.is_type(*exp.DataType.TEMPORAL_TYPES) and not (
4495                date_unit and timestamp.is_type(exp.DType.DATE)
4496            ):
4497                return self.sql(exp.Cast(this=result, to=timestamp.type))
4498
4499        return result
4500
4501    def trim_sql(self, expression: exp.Trim) -> str:
4502        expression.this.replace(_cast_to_varchar(expression.this))
4503        if expression.expression:
4504            expression.expression.replace(_cast_to_varchar(expression.expression))
4505
4506        result_sql = super().trim_sql(expression)
4507        return _gen_with_cast_to_blob(self, expression, result_sql)
4508
4509    def round_sql(self, expression: exp.Round) -> str:
4510        this = expression.this
4511        decimals = expression.args.get("decimals")
4512        truncate = expression.args.get("truncate")
4513
4514        # DuckDB requires the scale (decimals) argument to be an INT
4515        # Some dialects (e.g., Snowflake) allow non-integer scales and cast to an integer internally
4516        if decimals is not None and expression.args.get("casts_non_integer_decimals"):
4517            if not (decimals.is_int or decimals.is_type(*exp.DataType.INTEGER_TYPES)):
4518                decimals = exp.cast(decimals, exp.DType.INT)
4519
4520        func = "ROUND"
4521        if truncate:
4522            # BigQuery uses ROUND_HALF_EVEN; Snowflake uses HALF_TO_EVEN
4523            if truncate.this in ("ROUND_HALF_EVEN", "HALF_TO_EVEN"):
4524                func = "ROUND_EVEN"
4525                truncate = None
4526            # BigQuery uses ROUND_HALF_AWAY_FROM_ZERO; Snowflake uses HALF_AWAY_FROM_ZERO
4527            elif truncate.this in ("ROUND_HALF_AWAY_FROM_ZERO", "HALF_AWAY_FROM_ZERO"):
4528                truncate = None
4529
4530        return self.func(func, this, decimals, truncate)
4531
4532    def trycast_sql(self, expression: exp.TryCast) -> str:
4533        to = expression.to
4534        to_type = to.this
4535        src = expression.this
4536
4537        if (
4538            expression.args.get("null_on_text_overflow")
4539            and to_type in exp.DataType.TEXT_TYPES
4540            and to.expressions
4541        ):
4542            return self.sql(
4543                exp.case()
4544                .when(
4545                    exp.LTE(this=exp.func("LENGTH", src), expression=to.expressions[0].this),
4546                    exp.cast(src, "TEXT"),
4547                )
4548                .else_(exp.Null())
4549            )
4550        elif to_type == exp.DType.DATE and expression.args.get("probe_date_format"):
4551            slash_strptime = exp.cast(
4552                exp.func("TRY_STRPTIME", src, exp.Literal.string(self._TRYCAST_DATE_SLASH_FMT)),
4553                "DATE",
4554            )
4555            mon_strptime = exp.cast(
4556                exp.func("TRY_STRPTIME", src, exp.Literal.string(self._TRYCAST_DATE_MON_FMT)),
4557                "DATE",
4558            )
4559            return self.sql(
4560                exp.case()
4561                .when(exp.func("CONTAINS", src, exp.Literal.string("/")), slash_strptime)
4562                .when(
4563                    exp.RegexpLike(this=src, expression=exp.Literal.string("[A-Za-z]")),
4564                    mon_strptime,
4565                )
4566                .else_(exp.TryCast(this=src, to=to))
4567            )
4568        elif (
4569            isinstance(to_type, exp.Interval)
4570            and (unit := to_type.unit)
4571            and expression.args.get("requires_string")
4572        ):
4573            interval_type = exp.DataType.build("INTERVAL")
4574            if isinstance(unit, exp.IntervalSpan):
4575                self.unsupported(
4576                    "TRY_CAST to INTERVAL with span (e.g. HOUR TO MINUTE) is not supported in DuckDB"
4577                )
4578                return self.sql(exp.TryCast(this=src, to=interval_type))
4579            return self.sql(
4580                exp.TryCast(
4581                    this=exp.DPipe(this=src, expression=exp.Literal.string(f" {unit.name}")),
4582                    to=interval_type,
4583                )
4584            )
4585
4586        return super().trycast_sql(expression)
4587
4588    def strtok_sql(self, expression: exp.Strtok) -> str:
4589        string_arg = expression.this
4590        delimiter_arg = expression.args.get("delimiter")
4591        part_index_arg = expression.args.get("part_index")
4592
4593        if delimiter_arg and part_index_arg:
4594            # Escape regex chars and build character class at runtime using REGEXP_REPLACE
4595            escaped_delimiter = exp.Anonymous(
4596                this="REGEXP_REPLACE",
4597                expressions=[
4598                    delimiter_arg,
4599                    exp.Literal.string(
4600                        r"([\[\]^.\-*+?(){}|$\\])"
4601                    ),  # Escape problematic regex chars
4602                    exp.Literal.string(
4603                        r"\\\1"
4604                    ),  # Replace with escaped version using $1 backreference
4605                    exp.Literal.string("g"),  # Global flag
4606                ],
4607            )
4608            # CASE WHEN delimiter = '' THEN '' ELSE CONCAT('[', escaped_delimiter, ']') END
4609            regex_pattern = (
4610                exp.case()
4611                .when(delimiter_arg.eq(exp.Literal.string("")), exp.Literal.string(""))
4612                .else_(
4613                    exp.func(
4614                        "CONCAT",
4615                        exp.Literal.string("["),
4616                        escaped_delimiter,
4617                        exp.Literal.string("]"),
4618                    )
4619                )
4620            )
4621
4622            # STRTOK skips empty strings, so we need to filter them out
4623            # LIST_FILTER(REGEXP_SPLIT_TO_ARRAY(string, pattern), x -> x != '')[index]
4624            split_array = exp.func("REGEXP_SPLIT_TO_ARRAY", string_arg, regex_pattern)
4625            x = exp.to_identifier("x")
4626            is_empty = x.eq(exp.Literal.string(""))
4627            filtered_array = exp.func(
4628                "LIST_FILTER",
4629                split_array,
4630                exp.Lambda(this=exp.not_(is_empty.copy()), expressions=[x.copy()]),
4631            )
4632            base_func = exp.Bracket(
4633                this=filtered_array,
4634                expressions=[part_index_arg],
4635                offset=1,
4636            )
4637
4638            # Use template with the built regex pattern
4639            result = exp.replace_placeholders(
4640                self.STRTOK_TEMPLATE.copy(),
4641                string=string_arg,
4642                delimiter=delimiter_arg,
4643                part_index=part_index_arg,
4644                base_func=base_func,
4645            )
4646
4647            return self.sql(result)
4648
4649        return self.function_fallback_sql(expression)
4650
4651    def strtoktoarray_sql(self, expression: exp.StrtokToArray) -> str:
4652        string_arg = expression.this
4653        delimiter_arg = expression.args.get("expression") or exp.Literal.string(" ")
4654
4655        escaped = exp.RegexpReplace(
4656            this=delimiter_arg.copy(),
4657            expression=exp.Literal.string(r"([\[\]^.\-*+?(){}|$\\])"),
4658            replacement=exp.Literal.string(r"\\\1"),
4659            modifiers=exp.Literal.string("g"),
4660        )
4661        return self.sql(
4662            exp.replace_placeholders(
4663                self.STRTOK_TO_ARRAY_TEMPLATE.copy(),
4664                string=string_arg,
4665                delimiter=delimiter_arg,
4666                escaped=escaped,
4667            )
4668        )
4669
4670    def approxquantile_sql(self, expression: exp.ApproxQuantile) -> str:
4671        result = self.func("APPROX_QUANTILE", expression.this, expression.args.get("quantile"))
4672
4673        # DuckDB returns integers for APPROX_QUANTILE, cast to DOUBLE if the expected type is a real type
4674        if expression.is_type(*exp.DataType.REAL_TYPES):
4675            result = f"CAST({result} AS DOUBLE)"
4676
4677        return result
4678
4679    def approxquantiles_sql(self, expression: exp.ApproxQuantiles) -> str:
4680        """
4681        BigQuery's APPROX_QUANTILES(expr, n) returns an array of n+1 approximate quantile values
4682        dividing the input distribution into n equal-sized buckets.
4683
4684        Both BigQuery and DuckDB use approximate algorithms for quantile estimation, but BigQuery
4685        does not document the specific algorithm used so results may differ. DuckDB does not
4686        support RESPECT NULLS.
4687        """
4688        this = expression.this
4689        if isinstance(this, exp.Distinct):
4690            # APPROX_QUANTILES requires 2 args and DISTINCT node grabs both
4691            if len(this.expressions) < 2:
4692                self.unsupported("APPROX_QUANTILES requires a bucket count argument")
4693                return self.function_fallback_sql(expression)
4694            num_quantiles_expr = this.expressions[1].pop()
4695        else:
4696            num_quantiles_expr = expression.expression
4697
4698        if not isinstance(num_quantiles_expr, exp.Literal) or not num_quantiles_expr.is_int:
4699            self.unsupported("APPROX_QUANTILES bucket count must be a positive integer")
4700            return self.function_fallback_sql(expression)
4701
4702        num_quantiles = t.cast(int, num_quantiles_expr.to_py())
4703        if num_quantiles <= 0:
4704            self.unsupported("APPROX_QUANTILES bucket count must be a positive integer")
4705            return self.function_fallback_sql(expression)
4706
4707        quantiles = [
4708            exp.Literal.number(Decimal(i) / Decimal(num_quantiles))
4709            for i in range(num_quantiles + 1)
4710        ]
4711
4712        return self.sql(exp.ApproxQuantile(this=this, quantile=exp.Array(expressions=quantiles)))
4713
4714    def jsonextractscalar_sql(self, expression: exp.JSONExtractScalar) -> str:
4715        if expression.args.get("scalar_only"):
4716            json_value = exp.JSONExtractScalar(
4717                this=rename_func("JSON_VALUE")(self, expression), expression="'$'"
4718            )
4719
4720            # `->>` binds looser than most operators, so the wrap logic needs the parent
4721            json_value.parent = expression.parent
4722            expression = json_value
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: t.ClassVar[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: t.ClassVar[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: t.ClassVar[exp.Expr] = exp.maybe_parse(
1980        "(ABS(HASH(:seed)) % 1000000) / 1000000.0"
1981    )
1982
1983    # Template for generating signed and unsigned SEQ values within a specified range
1984    SEQ_UNSIGNED: t.ClassVar[exp.Expr] = _SEQ_UNSIGNED
1985    SEQ_SIGNED: t.ClassVar[exp.Expr] = _SEQ_SIGNED
1986
1987    # Template for MAP_CAT transpilation - Snowflake semantics:
1988    # 1. Returns NULL if either input is NULL
1989    # 2. For duplicate keys, prefers non-NULL value (COALESCE(m2[k], m1[k]))
1990    # 3. Filters out entries with NULL values from the result
1991    MAPCAT_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
1992        """
1993        CASE
1994            WHEN :map1 IS NULL OR :map2 IS NULL THEN NULL
1995            ELSE MAP_FROM_ENTRIES(LIST_FILTER(LIST_TRANSFORM(
1996                LIST_DISTINCT(LIST_CONCAT(MAP_KEYS(:map1), MAP_KEYS(:map2))),
1997                __k -> STRUCT_PACK(key := __k, value := COALESCE(:map2[__k], :map1[__k]))
1998            ), __x -> __x.value IS NOT NULL))
1999        END
2000        """
2001    )
2002
2003    # Mappings for EXTRACT/DATE_PART transpilation
2004    # Maps Snowflake specifiers unsupported in DuckDB to strftime format codes
2005    EXTRACT_STRFTIME_MAPPINGS: t.ClassVar[dict[str, tuple[str, str]]] = {
2006        "WEEKISO": ("%V", "INTEGER"),
2007        "YEAROFWEEK": ("%G", "INTEGER"),
2008        "YEAROFWEEKISO": ("%G", "INTEGER"),
2009        "NANOSECOND": ("%n", "BIGINT"),
2010    }
2011
2012    # Maps epoch-based specifiers to DuckDB epoch functions
2013    EXTRACT_EPOCH_MAPPINGS: t.ClassVar[dict[str, str]] = {
2014        "EPOCH_SECOND": "EPOCH",
2015        "EPOCH_MILLISECOND": "EPOCH_MS",
2016        "EPOCH_MICROSECOND": "EPOCH_US",
2017        "EPOCH_NANOSECOND": "EPOCH_NS",
2018    }
2019
2020    # Template for BITMAP_CONSTRUCT_AGG transpilation
2021    #
2022    # BACKGROUND:
2023    # Snowflake's BITMAP_CONSTRUCT_AGG aggregates integers into a compact binary bitmap.
2024    # Supports values in range 0-32767, this version returns NULL if any value is out of range
2025    # See: https://docs.snowflake.com/en/sql-reference/functions/bitmap_construct_agg
2026    # See: https://docs.snowflake.com/en/user-guide/querying-bitmaps-for-distinct-counts
2027    #
2028    # Snowflake uses two different formats based on the number of unique values:
2029    #
2030    # Format 1 - Small bitmap (< 5 unique values): Length of 10 bytes
2031    #   Bytes 0-1: Count of values as 2-byte big-endian integer (e.g., 3 values = 0x0003)
2032    #   Bytes 2-9: Up to 4 values, each as 2-byte little-endian integers, zero-padded to 8 bytes
2033    #   Example: Values [1, 2, 3] -> 0x0003 0100 0200 0300 0000 (hex)
2034    #                                count  v1   v2   v3   pad
2035    #
2036    # Format 2 - Large bitmap (>= 5 unique values): Length of 10 + (2 * count) bytes
2037    #   Bytes 0-9: Fixed header 0x08 followed by 9 zero bytes
2038    #   Bytes 10+: Each value as 2-byte little-endian integer (no padding)
2039    #   Example: Values [1,2,3,4,5] -> 0x08 00000000 00000000 00 0100 0200 0300 0400 0500
2040    #                                  hdr  ----9 zero bytes----  v1   v2   v3   v4   v5
2041    #
2042    # TEMPLATE STRUCTURE
2043    #
2044    # Phase 1 - Innermost subquery: Data preparation
2045    #   SELECT LIST_SORT(...) AS l
2046    #   - Aggregates all input values into a list, remove NULLs, duplicates and sorts
2047    #   Result: Clean, sorted list of unique non-null integers stored as 'l'
2048    #
2049    # Phase 2 - Middle subquery: Hex string construction
2050    #   LIST_TRANSFORM(...)
2051    #   - Converts each integer to 2-byte little-endian hex representation
2052    #   - & 255 extracts low byte, >> 8 extracts high byte
2053    #   - LIST_REDUCE: Concatenates all hex pairs into single string 'h'
2054    #   Result: Hex string of all values
2055    #
2056    # Phase 3 - Outer SELECT: Final bitmap assembly
2057    #   LENGTH(l) < 5:
2058    #   - Small format: 2-byte count (big-endian via %04X) + values + zero padding
2059    #   LENGTH(l) >= 5:
2060    #   - Large format: Fixed 10-byte header + values (no padding needed)
2061    #   Result: Complete binary bitmap as BLOB
2062    #
2063    BITMAP_CONSTRUCT_AGG_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2064        """
2065        SELECT CASE
2066            WHEN l IS NULL OR LENGTH(l) = 0 THEN NULL
2067            WHEN LENGTH(l) != LENGTH(LIST_FILTER(l, __v -> __v BETWEEN 0 AND 32767)) THEN NULL
2068            WHEN LENGTH(l) < 5 THEN UNHEX(PRINTF('%04X', LENGTH(l)) || h || REPEAT('00', GREATEST(0, 4 - LENGTH(l)) * 2))
2069            ELSE UNHEX('08000000000000000000' || h)
2070        END
2071        FROM (
2072            SELECT l, COALESCE(LIST_REDUCE(
2073                LIST_TRANSFORM(l, __x -> PRINTF('%02X%02X', CAST(__x AS INT) & 255, (CAST(__x AS INT) >> 8) & 255)),
2074                (__a, __b) -> __a || __b, ''
2075            ), '') AS h
2076            FROM (SELECT LIST_SORT(LIST_DISTINCT(LIST(:arg) FILTER(NOT :arg IS NULL))) AS l)
2077        )
2078        """
2079    )
2080
2081    # Template for RANDSTR transpilation - placeholders get replaced with actual parameters
2082    RANDSTR_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2083        f"""
2084        SELECT LISTAGG(
2085            SUBSTRING(
2086                '{RANDSTR_CHAR_POOL}',
2087                1 + CAST(FLOOR(random_value * 62) AS INT),
2088                1
2089            ),
2090            ''
2091        )
2092        FROM (
2093            SELECT (ABS(HASH(i + :seed)) % 1000) / 1000.0 AS random_value
2094            FROM RANGE(:length) AS t(i)
2095        )
2096        """,
2097    )
2098
2099    # Template for MINHASH transpilation
2100    # Computes k minimum hash values across aggregated data using DuckDB list functions
2101    # Returns JSON matching Snowflake format: {"state": [...], "type": "minhash", "version": 1}
2102    MINHASH_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2103        """
2104        SELECT JSON_OBJECT('state', LIST(min_h ORDER BY seed), 'type', 'minhash', 'version', 1)
2105        FROM (
2106            SELECT seed, LIST_MIN(LIST_TRANSFORM(vals, __v -> HASH(CAST(__v AS VARCHAR) || CAST(seed AS VARCHAR)))) AS min_h
2107            FROM (SELECT LIST(:expr) AS vals), RANGE(0, :k) AS t(seed)
2108        )
2109        """,
2110    )
2111
2112    # Template for MINHASH_COMBINE transpilation
2113    # Combines multiple minhash signatures by taking element-wise minimum
2114    MINHASH_COMBINE_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2115        """
2116        SELECT JSON_OBJECT('state', LIST(min_h ORDER BY idx), 'type', 'minhash', 'version', 1)
2117        FROM (
2118            SELECT
2119                pos AS idx,
2120                MIN(val) AS min_h
2121            FROM
2122                UNNEST(LIST(:expr)) AS _(sig),
2123                UNNEST(CAST(sig -> 'state' AS UBIGINT[])) WITH ORDINALITY AS t(val, pos)
2124            GROUP BY pos
2125        )
2126        """,
2127    )
2128
2129    # Template for APPROXIMATE_SIMILARITY transpilation
2130    # Computes multi-way Jaccard similarity: fraction of positions where ALL signatures agree
2131    APPROXIMATE_SIMILARITY_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2132        """
2133        SELECT CAST(SUM(CASE WHEN num_distinct = 1 THEN 1 ELSE 0 END) AS DOUBLE) / COUNT(*)
2134        FROM (
2135            SELECT pos, COUNT(DISTINCT h) AS num_distinct
2136            FROM (
2137                SELECT h, pos
2138                FROM UNNEST(LIST(:expr)) AS _(sig),
2139                     UNNEST(CAST(sig -> 'state' AS UBIGINT[])) WITH ORDINALITY AS s(h, pos)
2140            )
2141            GROUP BY pos
2142        )
2143        """,
2144    )
2145
2146    # Template for ARRAYS_ZIP transpilation
2147    # Snowflake pads to longest array; DuckDB LIST_ZIP truncates to shortest
2148    # Uses RANGE + indexing to match Snowflake behavior
2149    ARRAYS_ZIP_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2150        """
2151        CASE WHEN :null_check THEN NULL
2152        WHEN :all_empty_check THEN [:empty_struct]
2153        ELSE LIST_TRANSFORM(RANGE(0, :max_len), __i -> :transform_struct)
2154        END
2155        """,
2156    )
2157
2158    UUID_V5_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2159        """
2160        (SELECT
2161            LOWER(
2162                SUBSTR(h, 1, 8) || '-' ||
2163                SUBSTR(h, 9, 4) || '-' ||
2164                '5' || SUBSTR(h, 14, 3) || '-' ||
2165                FORMAT('{:02x}', CAST('0x' || SUBSTR(h, 17, 2) AS INT) & 63 | 128) || SUBSTR(h, 19, 2) || '-' ||
2166                SUBSTR(h, 21, 12)
2167            )
2168        FROM (
2169            SELECT SUBSTR(SHA1(UNHEX(REPLACE(:namespace, '-', '')) || ENCODE(:name, 'utf8')), 1, 32) AS h
2170        ))
2171        """
2172    )
2173
2174    # Shared bag semantics outer frame for ARRAY_EXCEPT and ARRAY_INTERSECTION.
2175    # Each element is paired with its 1-based position via LIST_ZIP, then filtered
2176    # by a comparison operator (supplied via :cond) that determines the operation:
2177    #   EXCEPT (>):        keep the N-th occurrence only if N > count in arr2
2178    #                      e.g. [2,2,2] EXCEPT [2,2] -> [2]
2179    #   INTERSECTION (<=): keep the N-th occurrence only if N <= count in arr2
2180    #                      e.g. [2,2,2] INTERSECT [2,2] -> [2,2]
2181    # IS NOT DISTINCT FROM is used for NULL-safe element comparison.
2182    ARRAY_BAG_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2183        """
2184        CASE
2185            WHEN :arr1 IS NULL OR :arr2 IS NULL THEN NULL
2186            ELSE LIST_TRANSFORM(
2187                LIST_FILTER(
2188                    LIST_ZIP(:arr1, GENERATE_SERIES(1, LEN(:arr1))),
2189                    pair -> :cond
2190                ),
2191                pair -> pair[0]
2192            )
2193        END
2194        """
2195    )
2196
2197    ARRAY_EXCEPT_CONDITION: t.ClassVar[exp.Expr] = exp.maybe_parse(
2198        "LEN(LIST_FILTER(:arr1[1:pair[1]], e -> e IS NOT DISTINCT FROM pair[0]))"
2199        " > LEN(LIST_FILTER(:arr2, e -> e IS NOT DISTINCT FROM pair[0]))"
2200    )
2201
2202    ARRAY_INTERSECTION_CONDITION: t.ClassVar[exp.Expr] = exp.maybe_parse(
2203        "LEN(LIST_FILTER(:arr1[1:pair[1]], e -> e IS NOT DISTINCT FROM pair[0]))"
2204        " <= LEN(LIST_FILTER(:arr2, e -> e IS NOT DISTINCT FROM pair[0]))"
2205    )
2206
2207    # Set semantics for ARRAY_EXCEPT. Deduplicates arr1 via LIST_DISTINCT, then
2208    # filters out any element that appears at least once in arr2.
2209    #   e.g. [1,1,2,3] EXCEPT [1] -> [2,3]
2210    # IS NOT DISTINCT FROM is used for NULL-safe element comparison.
2211    ARRAY_EXCEPT_SET_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2212        """
2213        CASE
2214            WHEN :arr1 IS NULL OR :arr2 IS NULL THEN NULL
2215            ELSE LIST_FILTER(
2216                LIST_DISTINCT(:arr1),
2217                e -> LEN(LIST_FILTER(:arr2, x -> x IS NOT DISTINCT FROM e)) = 0
2218            )
2219        END
2220        """
2221    )
2222
2223    # BigQuery's `x IN UNNEST(arr)` NULL semantics:
2224    #   NULL IN UNNEST([1, 2])  -> NULL
2225    #   3 IN UNNEST([1, NULL])  -> NULL
2226    #   3 IN UNNEST([1, 2])     -> FALSE
2227    #   1 IN UNNEST(NULL)       -> FALSE (not NULL)
2228    #   1 IN UNNEST([])         -> FALSE
2229    # The default `IN (SELECT UNNEST(...))` rewrite creates a correlated subquery
2230    # that DuckDB rejects inside non-inner joins, so a CASE expression is used instead.
2231    IN_UNNEST_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2232        """
2233        CASE
2234            WHEN :arr IS NULL OR ARRAY_LENGTH(:arr) = 0 THEN FALSE
2235            WHEN ARRAY_CONTAINS(:arr, :value) THEN TRUE
2236            WHEN :value IS NULL OR ARRAY_LENGTH(:arr) <> LIST_COUNT(:arr) THEN NULL
2237            ELSE FALSE
2238        END
2239        """
2240    )
2241
2242    STRTOK_TO_ARRAY_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2243        """
2244        CASE WHEN :delimiter IS NULL THEN NULL
2245        ELSE LIST_FILTER(
2246            REGEXP_SPLIT_TO_ARRAY(:string, CASE WHEN :delimiter = '' THEN '.^' ELSE CONCAT('[', :escaped, ']') END),
2247            x -> NOT x = ''
2248        ) END
2249        """
2250    )
2251
2252    # Template for STRTOK function transpilation
2253    #
2254    # DuckDB itself doesn't have a strtok function. This handles the transpilation from Snowflake to DuckDB.
2255    # We may need to adjust this if we want to support transpilation from other dialects
2256    #
2257    # CASE
2258    #     -- Snowflake: empty delimiter + empty input string -> NULL
2259    #     WHEN delimiter = '' AND input_str = '' THEN NULL
2260    #
2261    #     -- Snowflake: empty delimiter + non-empty input string -> treats whole input as 1 token -> return input string if index is 1
2262    #     WHEN delimiter = '' AND index = 1 THEN input_str
2263    #
2264    #     -- Snowflake: empty delimiter + non-empty input string -> treats whole input as 1 token -> return NULL if index is not 1
2265    #     WHEN delimiter = '' THEN NULL
2266    #
2267    #     -- Snowflake: negative indices return NULL
2268    #     WHEN index < 0 THEN NULL
2269    #
2270    #     -- Snowflake: return NULL if any argument is NULL
2271    #     WHEN input_str IS NULL OR delimiter IS NULL OR index IS NULL THEN NULL
2272    #
2273    #
2274    #     ELSE LIST_FILTER(
2275    #         REGEXP_SPLIT_TO_ARRAY(
2276    #             input_str,
2277    #             CASE
2278    #                 -- if delimiter is '', we don't want to surround it with '[' and ']' as '[]' is invalid for DuckDB
2279    #                 WHEN delimiter = '' THEN ''
2280    #
2281    #                 -- handle problematic regex characters in delimiter with REGEXP_REPLACE
2282    #                 -- turn delimiter into a regex char set, otherwise DuckDB will match in order, which we don't want
2283    #                 ELSE '[' || REGEXP_REPLACE(delimiter, problematic_char_set, '\\\1', 'g') || ']'
2284    #             END
2285    #         ),
2286    #
2287    #         -- Snowflake: don't return empty strings
2288    #         x -> NOT x = ''
2289    #     )[index]
2290    # END
2291    STRTOK_TEMPLATE: t.ClassVar[exp.Expr] = exp.maybe_parse(
2292        """
2293        CASE
2294            WHEN :delimiter = '' AND :string = '' THEN NULL
2295            WHEN :delimiter = '' AND :part_index = 1 THEN :string
2296            WHEN :delimiter = '' THEN NULL
2297            WHEN :part_index < 0 THEN NULL
2298            WHEN :string IS NULL OR :delimiter IS NULL OR :part_index IS NULL THEN NULL
2299            ELSE :base_func
2300        END
2301        """
2302    )
2303
2304    # Snowflake AUTO detects 3 DATE formats: YYYY-MM-DD (ISO-8601), MM/DD/YYYY, DD-MON-YYYY.
2305    # DuckDB TRY_CAST handles ISO-8601 natively. For the other two formats we use CONTAINS('/')
2306    # and REGEXP_MATCHES('[A-Za-z]') as heuristics — these correctly handle single-digit months
2307    # and days (e.g. 1/5/2020, 5-JAN-2020) where a positional char check would fail.
2308    # Ref: https://docs.snowflake.com/en/sql-reference/date-time-input-output#date-formats
2309    _TRYCAST_DATE_SLASH_FMT = "%m/%d/%Y"
2310    _TRYCAST_DATE_MON_FMT = "%d-%b-%Y"
2311
2312    def _array_bag_sql(self, condition: exp.Expr, arr1: exp.Expr, arr2: exp.Expr) -> str:
2313        cond = exp.Paren(this=exp.replace_placeholders(condition, arr1=arr1, arr2=arr2))
2314        return self.sql(
2315            exp.replace_placeholders(self.ARRAY_BAG_TEMPLATE, arr1=arr1, arr2=arr2, cond=cond)
2316        )
2317
2318    def timeslice_sql(self, expression: exp.TimeSlice) -> str:
2319        """
2320        Transform Snowflake's TIME_SLICE to DuckDB's time_bucket.
2321
2322        Snowflake: TIME_SLICE(date_expr, slice_length, 'UNIT' [, 'START'|'END'])
2323        DuckDB:    time_bucket(INTERVAL 'slice_length' UNIT, date_expr)
2324
2325        For 'END' kind, add the interval to get the end of the slice.
2326        For DATE type with 'END', cast result back to DATE to preserve type.
2327        """
2328        date_expr = expression.this
2329        slice_length = expression.expression
2330        unit = expression.unit
2331        kind = expression.text("kind").upper()
2332
2333        # Create INTERVAL expression: INTERVAL 'N' UNIT
2334        interval_expr = exp.Interval(this=slice_length, unit=unit)
2335
2336        # Create base time_bucket expression
2337        time_bucket_expr = exp.func("time_bucket", interval_expr, date_expr)
2338
2339        # Check if we need the end of the slice (default is start)
2340        if not kind == "END":
2341            # For 'START', return time_bucket directly
2342            return self.sql(time_bucket_expr)
2343
2344        # For 'END', add the interval to get end of slice
2345        add_expr = exp.Add(this=time_bucket_expr, expression=interval_expr.copy())
2346
2347        # If input is DATE type, cast result back to DATE to preserve type
2348        # DuckDB converts DATE to TIMESTAMP when adding intervals
2349        if date_expr.is_type(exp.DType.DATE):
2350            return self.sql(exp.cast(add_expr, exp.DType.DATE))
2351
2352        return self.sql(add_expr)
2353
2354    def bitmapbucketnumber_sql(self, expression: exp.BitmapBucketNumber) -> str:
2355        """
2356        Transpile BITMAP_BUCKET_NUMBER function from Snowflake to DuckDB equivalent.
2357
2358        Snowflake's BITMAP_BUCKET_NUMBER returns a 1-based bucket identifier where:
2359        - Each bucket covers 32,768 values
2360        - Bucket numbering starts at 1
2361        - Formula: ((value - 1) // 32768) + 1 for positive values
2362
2363        For non-positive values (0 and negative), we use value // 32768 to avoid
2364        producing bucket 0 or positive bucket IDs for negative inputs.
2365        """
2366        value = expression.this
2367
2368        positive_formula = ((value - 1) // 32768) + 1
2369        non_positive_formula = value // 32768
2370
2371        # CASE WHEN value > 0 THEN ((value - 1) // 32768) + 1 ELSE value // 32768 END
2372        case_expr = (
2373            exp.case()
2374            .when(exp.GT(this=value, expression=exp.Literal.number(0)), positive_formula)
2375            .else_(non_positive_formula)
2376        )
2377        return self.sql(case_expr)
2378
2379    def bitmapbitposition_sql(self, expression: exp.BitmapBitPosition) -> str:
2380        """
2381        Transpile Snowflake's BITMAP_BIT_POSITION to DuckDB CASE expression.
2382
2383        Snowflake's BITMAP_BIT_POSITION behavior:
2384        - For n <= 0: returns ABS(n) % 32768
2385        - For n > 0: returns (n - 1) % 32768 (maximum return value is 32767)
2386        """
2387        this = expression.this
2388
2389        return self.sql(
2390            exp.Mod(
2391                this=exp.Paren(
2392                    this=exp.If(
2393                        this=exp.GT(this=this, expression=exp.Literal.number(0)),
2394                        true=this - exp.Literal.number(1),
2395                        false=exp.Abs(this=this),
2396                    )
2397                ),
2398                expression=MAX_BIT_POSITION,
2399            )
2400        )
2401
2402    def bitmapconstructagg_sql(self, expression: exp.BitmapConstructAgg) -> str:
2403        """
2404        Transpile Snowflake's BITMAP_CONSTRUCT_AGG to DuckDB equivalent.
2405        Uses a pre-parsed template with placeholders replaced by expression nodes.
2406
2407        Snowflake bitmap format:
2408        - Small (< 5 unique values): 2-byte count (big-endian) + values (little-endian) + padding to 10 bytes
2409        - Large (>= 5 unique values): 10-byte header (0x08 + 9 zeros) + values (little-endian)
2410        """
2411        arg = expression.this
2412        return (
2413            f"({self.sql(exp.replace_placeholders(self.BITMAP_CONSTRUCT_AGG_TEMPLATE, arg=arg))})"
2414        )
2415
2416    def getignorecase_sql(self, expression: exp.GetIgnoreCase) -> str:
2417        self.unsupported("DuckDB does not support the GET_IGNORE_CASE() function")
2418        return self.function_fallback_sql(expression)
2419
2420    def compress_sql(self, expression: exp.Compress) -> str:
2421        self.unsupported("DuckDB does not support the COMPRESS() function")
2422        return self.function_fallback_sql(expression)
2423
2424    def encrypt_sql(self, expression: exp.Encrypt) -> str:
2425        self.unsupported("ENCRYPT is not supported in DuckDB")
2426        return self.function_fallback_sql(expression)
2427
2428    def decrypt_sql(self, expression: exp.Decrypt) -> str:
2429        func_name = "TRY_DECRYPT" if expression.args.get("safe") else "DECRYPT"
2430        self.unsupported(f"{func_name} is not supported in DuckDB")
2431        return self.function_fallback_sql(expression)
2432
2433    def decryptraw_sql(self, expression: exp.DecryptRaw) -> str:
2434        func_name = "TRY_DECRYPT_RAW" if expression.args.get("safe") else "DECRYPT_RAW"
2435        self.unsupported(f"{func_name} is not supported in DuckDB")
2436        return self.function_fallback_sql(expression)
2437
2438    def encryptraw_sql(self, expression: exp.EncryptRaw) -> str:
2439        self.unsupported("ENCRYPT_RAW is not supported in DuckDB")
2440        return self.function_fallback_sql(expression)
2441
2442    def parseurl_sql(self, expression: exp.ParseUrl) -> str:
2443        self.unsupported("PARSE_URL is not supported in DuckDB")
2444        return self.function_fallback_sql(expression)
2445
2446    def parseip_sql(self, expression: exp.ParseIp) -> str:
2447        self.unsupported("PARSE_IP is not supported in DuckDB")
2448        return self.function_fallback_sql(expression)
2449
2450    def decompressstring_sql(self, expression: exp.DecompressString) -> str:
2451        self.unsupported("DECOMPRESS_STRING is not supported in DuckDB")
2452        return self.function_fallback_sql(expression)
2453
2454    def decompressbinary_sql(self, expression: exp.DecompressBinary) -> str:
2455        self.unsupported("DECOMPRESS_BINARY is not supported in DuckDB")
2456        return self.function_fallback_sql(expression)
2457
2458    def jarowinklersimilarity_sql(self, expression: exp.JarowinklerSimilarity) -> str:
2459        this = expression.this
2460        expr = expression.expression
2461
2462        if expression.args.get("case_insensitive"):
2463            this = exp.Upper(this=this)
2464            expr = exp.Upper(this=expr)
2465
2466        result = exp.func("JARO_WINKLER_SIMILARITY", this, expr)
2467
2468        if expression.args.get("integer_scale"):
2469            result = exp.cast(result * 100, "INTEGER")
2470
2471        return self.sql(result)
2472
2473    def randstr_sql(self, expression: exp.Randstr) -> str:
2474        """
2475        Transpile Snowflake's RANDSTR to DuckDB equivalent using deterministic hash-based random.
2476        Uses a pre-parsed template with placeholders replaced by expression nodes.
2477
2478        RANDSTR(length, generator) generates a random string of specified length.
2479        - With numeric seed: Use HASH(i + seed) for deterministic output (same seed = same result)
2480        - With RANDOM(): Use RANDOM() in the hash for non-deterministic output
2481        - No generator: Use default seed value
2482        """
2483        length = expression.this
2484        generator = expression.args.get("generator")
2485
2486        if generator:
2487            if isinstance(generator, exp.Rand):
2488                # If it's RANDOM(), use its seed if available, otherwise use RANDOM() itself
2489                seed_value = generator.this or generator
2490            else:
2491                # Const/int or other expression - use as seed directly
2492                seed_value = generator
2493        else:
2494            # No generator specified, use default seed (arbitrary but deterministic)
2495            seed_value = exp.Literal.number(RANDSTR_SEED)
2496
2497        replacements = {"seed": seed_value, "length": length}
2498        return f"({self.sql(exp.replace_placeholders(self.RANDSTR_TEMPLATE, **replacements))})"
2499
2500    @unsupported_args("finish")
2501    def reduce_sql(self, expression: exp.Reduce) -> str:
2502        array_arg = expression.this
2503        initial_value = expression.args.get("initial")
2504        merge_lambda = expression.args.get("merge")
2505
2506        if merge_lambda:
2507            merge_lambda.set("colon", True)
2508
2509        return self.func("list_reduce", array_arg, merge_lambda, initial_value)
2510
2511    def zipf_sql(self, expression: exp.Zipf) -> str:
2512        """
2513        Transpile Snowflake's ZIPF to DuckDB using CDF-based inverse sampling.
2514        Uses a pre-parsed template with placeholders replaced by expression nodes.
2515        """
2516        s = expression.this
2517        n = expression.args["elementcount"]
2518        gen = expression.args["gen"]
2519
2520        if not isinstance(gen, exp.Rand):
2521            # (ABS(HASH(seed)) % 1000000) / 1000000.0
2522            random_expr: exp.Expr = exp.Div(
2523                this=exp.Paren(
2524                    this=exp.Mod(
2525                        this=exp.Abs(this=exp.Anonymous(this="HASH", expressions=[gen.copy()])),
2526                        expression=exp.Literal.number(1000000),
2527                    )
2528                ),
2529                expression=exp.Literal.number(1000000.0),
2530            )
2531        else:
2532            # Use RANDOM() for non-deterministic output
2533            random_expr = exp.Rand()
2534
2535        replacements = {"s": s, "n": n, "random_expr": random_expr}
2536        return f"({self.sql(exp.replace_placeholders(self.ZIPF_TEMPLATE, **replacements))})"
2537
2538    def tobinary_sql(self, expression: exp.ToBinary) -> str:
2539        """
2540        TO_BINARY and TRY_TO_BINARY transpilation:
2541        - 'HEX': TO_BINARY('48454C50', 'HEX') -> UNHEX('48454C50')
2542        - 'UTF-8': TO_BINARY('TEST', 'UTF-8') -> ENCODE('TEST')
2543        - 'BASE64': TO_BINARY('SEVMUA==', 'BASE64') -> FROM_BASE64('SEVMUA==')
2544
2545        For TRY_TO_BINARY (safe=True), wrap with TRY():
2546        - 'HEX': TRY_TO_BINARY('invalid', 'HEX') -> TRY(UNHEX('invalid'))
2547        """
2548        value = expression.this
2549        format_arg = expression.args.get("format")
2550        is_safe = expression.args.get("safe")
2551        is_binary = _is_binary(expression)
2552
2553        if not format_arg and not is_binary:
2554            func_name = "TRY_TO_BINARY" if is_safe else "TO_BINARY"
2555            return self.func(func_name, value)
2556
2557        # Snowflake defaults to HEX encoding when no format is specified
2558        fmt = format_arg.name.upper() if format_arg else "HEX"
2559
2560        if fmt in ("UTF-8", "UTF8"):
2561            # DuckDB ENCODE always uses UTF-8, no charset parameter needed
2562            result = self.func("ENCODE", value)
2563        elif fmt == "BASE64":
2564            result = self.func("FROM_BASE64", value)
2565        elif fmt == "HEX":
2566            result = self.func("UNHEX", value)
2567        else:
2568            if is_safe:
2569                return self.sql(exp.null())
2570            else:
2571                self.unsupported(f"format {fmt} is not supported")
2572                result = self.func("TO_BINARY", value)
2573        return f"TRY({result})" if is_safe else result
2574
2575    def tonumber_sql(self, expression: exp.ToNumber) -> str:
2576        fmt = expression.args.get("format")
2577        precision = expression.args.get("precision")
2578        scale = expression.args.get("scale")
2579
2580        if not fmt and precision and scale:
2581            return self.sql(
2582                exp.cast(
2583                    expression.this, f"DECIMAL({precision.name}, {scale.name})", dialect="duckdb"
2584                )
2585            )
2586
2587        return super().tonumber_sql(expression)
2588
2589    def _greatest_least_sql(self, expression: exp.Greatest | exp.Least) -> str:
2590        """
2591        Handle GREATEST/LEAST functions with dialect-aware NULL behavior.
2592
2593        - If ignore_nulls=False (BigQuery-style): return NULL if any argument is NULL
2594        - If ignore_nulls=True (DuckDB/PostgreSQL-style): ignore NULLs, return greatest/least non-NULL value
2595        """
2596        # Get all arguments
2597        all_args = [expression.this, *expression.expressions]
2598        fallback_sql = self.function_fallback_sql(expression)
2599
2600        if expression.args.get("ignore_nulls"):
2601            # DuckDB/PostgreSQL behavior: use native GREATEST/LEAST (ignores NULLs)
2602            return self.sql(fallback_sql)
2603
2604        # return NULL if any argument is NULL
2605        case_expr = exp.case().when(
2606            exp.or_(*[arg.is_(exp.null()) for arg in all_args], copy=False),
2607            exp.null(),
2608            copy=False,
2609        )
2610        case_expr.set("default", fallback_sql)
2611        return self.sql(case_expr)
2612
2613    def generator_sql(self, expression: exp.Generator) -> str:
2614        # Transpile Snowflake GENERATOR to DuckDB range()
2615        rowcount = expression.args.get("rowcount")
2616        time_limit = expression.args.get("time_limit")
2617
2618        if time_limit:
2619            self.unsupported("GENERATOR TIMELIMIT parameter is not supported in DuckDB")
2620
2621        if not rowcount:
2622            self.unsupported("GENERATOR without ROWCOUNT is not supported in DuckDB")
2623            return self.func("range", exp.Literal.number(0))
2624
2625        return self.func("range", rowcount)
2626
2627    def greatest_sql(self, expression: exp.Greatest) -> str:
2628        return self._greatest_least_sql(expression)
2629
2630    def least_sql(self, expression: exp.Least) -> str:
2631        return self._greatest_least_sql(expression)
2632
2633    def lambda_sql(self, expression: exp.Lambda, arrow_sep: str = "->", wrap: bool = True) -> str:
2634        if expression.args.get("colon"):
2635            prefix = "LAMBDA "
2636            arrow_sep = ":"
2637            wrap = False
2638        else:
2639            prefix = ""
2640
2641        lambda_sql = super().lambda_sql(expression, arrow_sep=arrow_sep, wrap=wrap)
2642        return f"{prefix}{lambda_sql}"
2643
2644    def show_sql(self, expression: exp.Show) -> str:
2645        from_ = self.sql(expression, "from_")
2646        from_ = f" FROM {from_}" if from_ else ""
2647        return f"SHOW {expression.name}{from_}"
2648
2649    def soundex_sql(self, expression: exp.Soundex) -> str:
2650        self.unsupported("SOUNDEX is not supported in DuckDB")
2651        return self.func("SOUNDEX", expression.this)
2652
2653    def sortarray_sql(self, expression: exp.SortArray) -> str:
2654        arr = expression.this
2655        asc = expression.args.get("asc")
2656        nulls_first = expression.args.get("nulls_first")
2657
2658        if not isinstance(asc, exp.Boolean) and not isinstance(nulls_first, exp.Boolean):
2659            return self.func("LIST_SORT", arr, asc, nulls_first)
2660
2661        nulls_are_first = nulls_first == exp.true()
2662        nulls_first_sql = exp.Literal.string("NULLS FIRST") if nulls_are_first else None
2663
2664        if not isinstance(asc, exp.Boolean):
2665            return self.func("LIST_SORT", arr, asc, nulls_first_sql)
2666
2667        descending = asc == exp.false()
2668
2669        if not descending and not nulls_are_first:
2670            return self.func("LIST_SORT", arr)
2671        if not nulls_are_first:
2672            return self.func("ARRAY_REVERSE_SORT", arr)
2673        return self.func(
2674            "LIST_SORT",
2675            arr,
2676            exp.Literal.string("DESC" if descending else "ASC"),
2677            exp.Literal.string("NULLS FIRST"),
2678        )
2679
2680    def install_sql(self, expression: exp.Install) -> str:
2681        force = "FORCE " if expression.args.get("force") else ""
2682        this = self.sql(expression, "this")
2683        from_clause = expression.args.get("from_")
2684        from_clause = f" FROM {from_clause}" if from_clause else ""
2685        return f"{force}INSTALL {this}{from_clause}"
2686
2687    def approxtopk_sql(self, expression: exp.ApproxTopK) -> str:
2688        self.unsupported(
2689            "APPROX_TOP_K cannot be transpiled to DuckDB due to incompatible return types. "
2690        )
2691        return self.function_fallback_sql(expression)
2692
2693    def strposition_sql(self, expression: exp.StrPosition) -> str:
2694        this = expression.this
2695        substr = expression.args.get("substr")
2696        position = expression.args.get("position")
2697
2698        # For BINARY/BLOB: DuckDB's STRPOS doesn't support BLOB types
2699        # Convert to HEX strings, use STRPOS, then convert hex position to byte position
2700        if _is_binary(this):
2701            # Build expression: STRPOS(HEX(haystack), HEX(needle))
2702            hex_strpos = exp.StrPosition(
2703                this=exp.Hex(this=this),
2704                substr=exp.Hex(this=substr),
2705            )
2706
2707            return self.sql(exp.cast((hex_strpos + 1) / 2, exp.DType.INT))
2708
2709        # For VARCHAR: handle clamp_position
2710        if expression.args.get("clamp_position") and position:
2711            expression = expression.copy()
2712            expression.set(
2713                "position",
2714                exp.If(
2715                    this=exp.LTE(this=position, expression=exp.Literal.number(0)),
2716                    true=exp.Literal.number(1),
2717                    false=position.copy(),
2718                ),
2719            )
2720
2721        return strposition_sql(self, expression)
2722
2723    def substring_sql(self, expression: exp.Substring) -> str:
2724        if expression.args.get("zero_start"):
2725            start = expression.args.get("start")
2726            length = expression.args.get("length")
2727
2728            if start := expression.args.get("start"):
2729                start = exp.If(this=start.eq(0), true=exp.Literal.number(1), false=start)
2730            if length := expression.args.get("length"):
2731                length = exp.If(this=length < 0, true=exp.Literal.number(0), false=length)
2732
2733            return self.func("SUBSTRING", expression.this, start, length)
2734
2735        return self.function_fallback_sql(expression)
2736
2737    def strtotime_sql(self, expression: exp.StrToTime) -> str:
2738        # Check if target_type requires TIMESTAMPTZ (for LTZ/TZ variants)
2739        target_type = expression.args.get("target_type")
2740        needs_tz = target_type and target_type.this in (
2741            exp.DType.TIMESTAMPLTZ,
2742            exp.DType.TIMESTAMPTZ,
2743        )
2744
2745        value, formatted_time = self._strptime_default_year(expression)
2746
2747        if expression.args.get("safe"):
2748            cast_type = exp.DType.TIMESTAMPTZ if needs_tz else exp.DType.TIMESTAMP
2749            return self.sql(exp.cast(self.func("TRY_STRPTIME", value, formatted_time), cast_type))
2750
2751        base_sql = self.func("STRPTIME", value, formatted_time)
2752        if needs_tz:
2753            return self.sql(
2754                exp.cast(
2755                    base_sql,
2756                    exp.DataType(this=exp.DType.TIMESTAMPTZ),
2757                )
2758            )
2759        return base_sql
2760
2761    def strtodate_sql(self, expression: exp.StrToDate) -> str:
2762        value, formatted_time = self._strptime_default_year(expression)
2763        function_name = "STRPTIME" if not expression.args.get("safe") else "TRY_STRPTIME"
2764        return self.sql(
2765            exp.cast(
2766                self.func(function_name, value, formatted_time),
2767                exp.DataType(this=exp.DType.DATE),
2768            )
2769        )
2770
2771    def _strptime_default_year(
2772        self, expression: exp.StrToTime | exp.StrToDate | exp.ParseDatetime
2773    ) -> tuple[exp.ExpOrStr, exp.ExpOrStr | None]:
2774        value: exp.ExpOrStr = expression.this
2775        formatted_time: exp.ExpOrStr | None = self.format_time(expression)
2776
2777        if default_year := expression.args.get("default_year"):
2778            value = exp.DPipe(this=exp.Literal.string(f"{default_year.name} "), expression=value)
2779            formatted_time = exp.DPipe(this=exp.Literal.string("%Y "), expression=formatted_time)
2780
2781        return value, formatted_time
2782
2783    def parsedatetime_sql(self, expression: exp.ParseDatetime) -> str:
2784        value, formatted_time = self._strptime_default_year(expression)
2785        return self.func("STRPTIME", value, formatted_time)
2786
2787    def parsetime_sql(self, expression: exp.ParseTime) -> str:
2788        formatted_time = self.format_time(expression)
2789        return self.sql(
2790            exp.cast(
2791                self.func("STRPTIME", expression.this, formatted_time),
2792                exp.DataType(this=exp.DType.TIME),
2793            )
2794        )
2795
2796    def tsordstotime_sql(self, expression: exp.TsOrDsToTime) -> str:
2797        this = expression.this
2798        time_format = self.format_time(expression)
2799        safe = expression.args.get("safe")
2800        time_type = exp.DataType.from_str("TIME", dialect="duckdb")
2801        cast_expr = exp.TryCast if safe else exp.Cast
2802
2803        if time_format:
2804            func_name = "TRY_STRPTIME" if safe else "STRPTIME"
2805            strptime = exp.Anonymous(this=func_name, expressions=[this, time_format])
2806            return self.sql(cast_expr(this=strptime, to=time_type))
2807
2808        if isinstance(this, exp.TsOrDsToTime) or this.is_type(exp.DType.TIME):
2809            return self.sql(this)
2810
2811        return self.sql(cast_expr(this=this, to=time_type))
2812
2813    def currentdate_sql(self, expression: exp.CurrentDate) -> str:
2814        if not expression.this:
2815            return "CURRENT_DATE"
2816
2817        expr = exp.Cast(
2818            this=exp.AtTimeZone(this=exp.CurrentTimestamp(), zone=expression.this),
2819            to=exp.DataType(this=exp.DType.DATE),
2820        )
2821        return self.sql(expr)
2822
2823    def checkjson_sql(self, expression: exp.CheckJson) -> str:
2824        arg = expression.this
2825        return self.sql(
2826            exp.case()
2827            .when(
2828                exp.or_(arg.is_(exp.Null()), arg.eq(""), exp.func("json_valid", arg)),
2829                exp.null(),
2830            )
2831            .else_(exp.Literal.string("Invalid JSON"))
2832        )
2833
2834    def parsejson_sql(self, expression: exp.ParseJSON) -> str:
2835        arg = expression.this
2836        if expression.args.get("safe"):
2837            return self.sql(
2838                exp.case()
2839                .when(exp.func("json_valid", arg), exp.cast(arg.copy(), "JSON"))
2840                .else_(exp.null())
2841            )
2842        return self.func("JSON", arg)
2843
2844    def unicode_sql(self, expression: exp.Unicode) -> str:
2845        if expression.args.get("empty_is_zero"):
2846            return self.sql(
2847                exp.case()
2848                .when(expression.this.eq(exp.Literal.string("")), exp.Literal.number(0))
2849                .else_(exp.Anonymous(this="UNICODE", expressions=[expression.this]))
2850            )
2851
2852        return self.func("UNICODE", expression.this)
2853
2854    def stripnullvalue_sql(self, expression: exp.StripNullValue) -> str:
2855        return self.sql(
2856            exp.case()
2857            .when(exp.func("json_type", expression.this).eq("NULL"), exp.null())
2858            .else_(expression.this)
2859        )
2860
2861    def trunc_sql(self, expression: exp.Trunc) -> str:
2862        decimals = expression.args.get("decimals")
2863        if (
2864            expression.args.get("fractions_supported")
2865            and decimals
2866            and not decimals.is_type(exp.DType.INT)
2867        ):
2868            decimals = exp.cast(decimals, exp.DType.INT, dialect="duckdb")
2869
2870        return self.func("TRUNC", expression.this, decimals)
2871
2872    def normal_sql(self, expression: exp.Normal) -> str:
2873        """
2874        Transpile Snowflake's NORMAL(mean, stddev, gen) to DuckDB.
2875
2876        Uses the Box-Muller transform via NORMAL_TEMPLATE.
2877        """
2878        mean = expression.this
2879        stddev = expression.args["stddev"]
2880        gen: exp.Expr = expression.args["gen"]
2881
2882        # Build two uniform random values [0, 1) for Box-Muller transform
2883        if isinstance(gen, exp.Rand) and gen.this is None:
2884            u1: exp.Expr = exp.Rand()
2885            u2: exp.Expr = exp.Rand()
2886        else:
2887            # Seeded: derive two values using HASH with different inputs
2888            seed = gen.this if isinstance(gen, exp.Rand) else gen
2889            u1 = exp.replace_placeholders(self.SEEDED_RANDOM_TEMPLATE, seed=seed)
2890            u2 = exp.replace_placeholders(
2891                self.SEEDED_RANDOM_TEMPLATE,
2892                seed=exp.Add(this=seed.copy(), expression=exp.Literal.number(1)),
2893            )
2894
2895        replacements = {"mean": mean, "stddev": stddev, "u1": u1, "u2": u2}
2896        return self.sql(exp.replace_placeholders(self.NORMAL_TEMPLATE, **replacements))
2897
2898    def uniform_sql(self, expression: exp.Uniform) -> str:
2899        """
2900        Transpile Snowflake's UNIFORM(min, max, gen) to DuckDB.
2901
2902        UNIFORM returns a random value in [min, max]:
2903        - Integer result if both min and max are integers
2904        - Float result if either min or max is a float
2905        """
2906        min_val = expression.this
2907        max_val = expression.expression
2908        gen = expression.args.get("gen")
2909
2910        # Determine if result should be integer (both bounds are integers).
2911        # We do this to emulate Snowflake's behavior, INT -> INT, FLOAT -> FLOAT
2912        is_int_result = min_val.is_int and max_val.is_int
2913
2914        # Build the random value expression [0, 1)
2915        if not isinstance(gen, exp.Rand):
2916            # Seed value: (ABS(HASH(seed)) % 1000000) / 1000000.0
2917            random_expr: exp.Expr = exp.Div(
2918                this=exp.Paren(
2919                    this=exp.Mod(
2920                        this=exp.Abs(this=exp.Anonymous(this="HASH", expressions=[gen])),
2921                        expression=exp.Literal.number(1000000),
2922                    )
2923                ),
2924                expression=exp.Literal.number(1000000.0),
2925            )
2926        else:
2927            random_expr = exp.Rand()
2928
2929        # Build: min + random * (max - min [+ 1 for int])
2930        range_expr: exp.Expr = exp.Sub(this=max_val, expression=min_val)
2931        if is_int_result:
2932            range_expr = exp.Add(this=range_expr, expression=exp.Literal.number(1))
2933
2934        result: exp.Expr = exp.Add(
2935            this=min_val,
2936            expression=exp.Mul(this=random_expr, expression=exp.Paren(this=range_expr)),
2937        )
2938
2939        if is_int_result:
2940            result = exp.Cast(this=exp.Floor(this=result), to=exp.DType.BIGINT.into_expr())
2941
2942        return self.sql(result)
2943
2944    def timefromparts_sql(self, expression: exp.TimeFromParts) -> str:
2945        nano = expression.args.get("nano")
2946        overflow = expression.args.get("overflow")
2947
2948        # Snowflake's TIME_FROM_PARTS supports overflow
2949        if overflow:
2950            hour = expression.args["hour"]
2951            minute = expression.args["min"]
2952            sec = expression.args["sec"]
2953
2954            # Check if values are within normal ranges - use MAKE_TIME for efficiency
2955            if not nano and all(arg.is_int for arg in [hour, minute, sec]):
2956                try:
2957                    h_val = hour.to_py()
2958                    m_val = minute.to_py()
2959                    s_val = sec.to_py()
2960                    if 0 <= h_val <= 23 and 0 <= m_val <= 59 and 0 <= s_val <= 59:
2961                        return rename_func("MAKE_TIME")(self, expression)
2962                except ValueError:
2963                    pass
2964
2965            # Overflow or nanoseconds detected - use INTERVAL arithmetic
2966            if nano:
2967                sec = sec + nano.pop() / exp.Literal.number(1000000000.0)
2968
2969            total_seconds = hour * exp.Literal.number(3600) + minute * exp.Literal.number(60) + sec
2970
2971            return self.sql(
2972                exp.Add(
2973                    this=exp.Cast(
2974                        this=exp.Literal.string("00:00:00"), to=exp.DType.TIME.into_expr()
2975                    ),
2976                    expression=exp.Interval(this=total_seconds, unit=exp.var("SECOND")),
2977                )
2978            )
2979
2980        # Default: MAKE_TIME
2981        if nano:
2982            expression.set(
2983                "sec", expression.args["sec"] + nano.pop() / exp.Literal.number(1000000000.0)
2984            )
2985
2986        return rename_func("MAKE_TIME")(self, expression)
2987
2988    def extract_sql(self, expression: exp.Extract) -> str:
2989        """
2990        Transpile EXTRACT/DATE_PART for DuckDB, handling specifiers not natively supported.
2991
2992        DuckDB doesn't support: WEEKISO, YEAROFWEEK, YEAROFWEEKISO, NANOSECOND,
2993        EPOCH_SECOND (as integer), EPOCH_MILLISECOND, EPOCH_MICROSECOND, EPOCH_NANOSECOND
2994        """
2995        this = expression.this
2996        datetime_expr = expression.expression
2997
2998        # TIMESTAMPTZ extractions may produce different results between Snowflake and DuckDB
2999        # because Snowflake applies server timezone while DuckDB uses local timezone
3000        if datetime_expr.is_type(exp.DType.TIMESTAMPTZ, exp.DType.TIMESTAMPLTZ):
3001            self.unsupported(
3002                "EXTRACT from TIMESTAMPTZ / TIMESTAMPLTZ may produce different results due to timezone handling differences"
3003            )
3004
3005        part_name = this.name.upper()
3006
3007        if part_name in self.EXTRACT_STRFTIME_MAPPINGS:
3008            fmt, cast_type = self.EXTRACT_STRFTIME_MAPPINGS[part_name]
3009
3010            # Problem: strftime doesn't accept TIME and there's no NANOSECOND function
3011            # So, for NANOSECOND with TIME, fallback to MICROSECOND * 1000
3012            is_nano_time = part_name == "NANOSECOND" and datetime_expr.is_type(
3013                exp.DType.TIME, exp.DType.TIMETZ
3014            )
3015
3016            if is_nano_time:
3017                self.unsupported("Parameter NANOSECOND is not supported with TIME type in DuckDB")
3018                return self.sql(
3019                    exp.cast(
3020                        exp.Mul(
3021                            this=exp.Extract(this=exp.var("MICROSECOND"), expression=datetime_expr),
3022                            expression=exp.Literal.number(1000),
3023                        ),
3024                        exp.DataType.from_str(cast_type, dialect="duckdb"),
3025                    )
3026                )
3027
3028            # For NANOSECOND, cast to TIMESTAMP_NS to preserve nanosecond precision
3029            strftime_input = datetime_expr
3030            if part_name == "NANOSECOND":
3031                strftime_input = exp.cast(datetime_expr, exp.DType.TIMESTAMP_NS)
3032
3033            return self.sql(
3034                exp.cast(
3035                    exp.Anonymous(
3036                        this="STRFTIME",
3037                        expressions=[strftime_input, exp.Literal.string(fmt)],
3038                    ),
3039                    exp.DataType.from_str(cast_type, dialect="duckdb"),
3040                )
3041            )
3042
3043        if part_name in self.EXTRACT_EPOCH_MAPPINGS:
3044            func_name = self.EXTRACT_EPOCH_MAPPINGS[part_name]
3045            result: exp.Expr = exp.Anonymous(this=func_name, expressions=[datetime_expr])
3046            # EPOCH returns float, cast to BIGINT for integer result
3047            if part_name == "EPOCH_SECOND":
3048                result = exp.cast(result, exp.DataType.from_str("BIGINT", dialect="duckdb"))
3049            return self.sql(result)
3050
3051        return super().extract_sql(expression)
3052
3053    def timestampfromparts_sql(self, expression: exp.TimestampFromParts) -> str:
3054        # Check if this is the date/time expression form: TIMESTAMP_FROM_PARTS(date_expr, time_expr)
3055        date_expr = expression.this
3056        time_expr = expression.expression
3057
3058        if date_expr is not None and time_expr is not None:
3059            # In DuckDB, DATE + TIME produces TIMESTAMP
3060            return self.sql(exp.Add(this=date_expr, expression=time_expr))
3061
3062        # Component-based form: TIMESTAMP_FROM_PARTS(year, month, day, hour, minute, second, ...)
3063        sec = expression.args.get("sec")
3064        if sec is None:
3065            # This shouldn't happen with valid input, but handle gracefully
3066            return rename_func("MAKE_TIMESTAMP")(self, expression)
3067
3068        milli = expression.args.get("milli")
3069        if milli is not None:
3070            sec += milli.pop() / exp.Literal.number(1000.0)
3071
3072        nano = expression.args.get("nano")
3073        if nano is not None:
3074            sec += nano.pop() / exp.Literal.number(1000000000.0)
3075
3076        if milli or nano:
3077            expression.set("sec", sec)
3078
3079        return rename_func("MAKE_TIMESTAMP")(self, expression)
3080
3081    @unsupported_args("nano")
3082    def timestampltzfromparts_sql(self, expression: exp.TimestampLtzFromParts) -> str:
3083        # Pop nano so rename_func only passes args that MAKE_TIMESTAMP accepts
3084        if nano := expression.args.get("nano"):
3085            nano.pop()
3086
3087        timestamp = rename_func("MAKE_TIMESTAMP")(self, expression)
3088        return f"CAST({timestamp} AS TIMESTAMPTZ)"
3089
3090    @unsupported_args("nano")
3091    def timestamptzfromparts_sql(self, expression: exp.TimestampTzFromParts) -> str:
3092        # Extract zone before popping
3093        zone = expression.args.get("zone")
3094        # Pop zone and nano so rename_func only passes args that MAKE_TIMESTAMP accepts
3095        if zone:
3096            zone = zone.pop()
3097
3098        if nano := expression.args.get("nano"):
3099            nano.pop()
3100
3101        timestamp = rename_func("MAKE_TIMESTAMP")(self, expression)
3102
3103        if zone:
3104            # Use AT TIME ZONE to apply the explicit timezone
3105            return f"{timestamp} AT TIME ZONE {self.sql(zone)}"
3106
3107        return timestamp
3108
3109    def tablesample_sql(
3110        self,
3111        expression: exp.TableSample,
3112        tablesample_keyword: str | None = None,
3113    ) -> str:
3114        if not isinstance(expression.parent, exp.Select):
3115            # This sample clause only applies to a single source, not the entire resulting relation
3116            tablesample_keyword = "TABLESAMPLE"
3117
3118        if expression.args.get("size"):
3119            method = expression.args.get("method")
3120            if method and method.name.upper() != "RESERVOIR":
3121                self.unsupported(
3122                    f"Sampling method {method} is not supported with a discrete sample count, "
3123                    "defaulting to reservoir sampling"
3124                )
3125                expression.set("method", exp.var("RESERVOIR"))
3126
3127        return super().tablesample_sql(expression, tablesample_keyword=tablesample_keyword)
3128
3129    def in_sql(self, expression: exp.In) -> str:
3130        unnest = expression.args.get("unnest")
3131        if unnest:
3132            return self.sql(
3133                exp.replace_placeholders(
3134                    self.IN_UNNEST_TEMPLATE, arr=unnest.expressions[0], value=expression.this
3135                )
3136            )
3137        return super().in_sql(expression)
3138
3139    def join_sql(self, expression: exp.Join) -> str:
3140        if (
3141            not expression.args.get("using")
3142            and not expression.args.get("on")
3143            and not expression.method
3144            and (expression.kind in ("", "INNER", "OUTER"))
3145        ):
3146            # Some dialects support `LEFT/INNER JOIN UNNEST(...)` without an explicit ON clause
3147            # DuckDB doesn't, but we can just add a dummy ON clause that is always true
3148            if isinstance(expression.this, exp.Unnest):
3149                return super().join_sql(expression.on(exp.true()))
3150
3151            expression.set("side", None)
3152            expression.set("kind", None)
3153
3154        return super().join_sql(expression)
3155
3156    def countif_sql(self, expression: exp.CountIf) -> str:
3157        if self.dialect.version >= (1, 2):
3158            this = expression.this
3159            if expression.args.get("zero_on_all_null") and not isinstance(this, exp.Distinct):
3160                # DuckDB >= 1.2's COUNT_IF returns NULL when the condition is NULL on all rows,
3161                # so we wrap the condition in IS TRUE to preserve count-like semantics
3162                expression = exp.CountIf(this=exp.paren(this).is_(exp.true()))
3163            return self.function_fallback_sql(expression)
3164
3165        # https://github.com/tobymao/sqlglot/pull/4749
3166        return count_if_to_sum(self, expression)
3167
3168    def bracket_sql(self, expression: exp.Bracket) -> str:
3169        if self.dialect.version >= (1, 2):
3170            return super().bracket_sql(expression)
3171
3172        # https://duckdb.org/2025/02/05/announcing-duckdb-120.html#breaking-changes
3173        this = expression.this
3174        if isinstance(this, exp.Array):
3175            this.replace(exp.paren(this))
3176
3177        bracket = super().bracket_sql(expression)
3178
3179        if not expression.args.get("returns_list_for_maps"):
3180            if not this.type:
3181                from sqlglot.optimizer.annotate_types import annotate_types
3182
3183                this = annotate_types(this, dialect=self.dialect)
3184
3185            if this.is_type(exp.DType.MAP):
3186                bracket = f"({bracket})[1]"
3187
3188        return bracket
3189
3190    def withingroup_sql(self, expression: exp.WithinGroup) -> str:
3191        func = expression.this
3192
3193        # For ARRAY_AGG, DuckDB requires ORDER BY inside the function, not in WITHIN GROUP
3194        # Transform: ARRAY_AGG(x) WITHIN GROUP (ORDER BY y) -> ARRAY_AGG(x ORDER BY y)
3195        if isinstance(func, exp.ArrayAgg):
3196            if not isinstance(order := expression.expression, exp.Order):
3197                return self.sql(func)
3198
3199            # Save the original column for FILTER clause (before wrapping with Order)
3200            original_this = func.this
3201
3202            # Move ORDER BY inside ARRAY_AGG by wrapping its argument with Order
3203            # ArrayAgg.this should become Order(this=ArrayAgg.this, expressions=order.expressions)
3204            func.set(
3205                "this",
3206                exp.Order(
3207                    this=func.this.copy(),
3208                    expressions=order.expressions,
3209                ),
3210            )
3211
3212            # Generate the ARRAY_AGG function with ORDER BY and add FILTER clause if needed
3213            # Use original_this (not the Order-wrapped version) for the FILTER condition
3214            array_agg_sql = self.function_fallback_sql(func)
3215            return self._add_arrayagg_null_filter(array_agg_sql, func, original_this)
3216
3217        # For other functions (like PERCENTILES), use existing logic
3218        expression_sql = self.sql(expression, "expression")
3219
3220        if isinstance(func, exp.PERCENTILES):
3221            # Make the order key the first arg and slide the fraction to the right
3222            # https://duckdb.org/docs/sql/aggregates#ordered-set-aggregate-functions
3223            order_col = expression.find(exp.Ordered)
3224            if order_col:
3225                func.set("expression", func.this)
3226                func.set("this", order_col.this)
3227
3228        this = self.sql(expression, "this").rstrip(")")
3229
3230        return f"{this}{expression_sql})"
3231
3232    def length_sql(self, expression: exp.Length) -> str:
3233        arg = expression.this
3234
3235        # Dialects like BQ and Snowflake also accept binary values as args, so
3236        # DDB will attempt to infer the type or resort to case/when resolution
3237        if not expression.args.get("binary") or arg.is_string:
3238            return self.func("LENGTH", arg)
3239
3240        if not arg.type:
3241            from sqlglot.optimizer.annotate_types import annotate_types
3242
3243            arg = annotate_types(arg, dialect=self.dialect)
3244
3245        if arg.is_type(*exp.DataType.TEXT_TYPES):
3246            return self.func("LENGTH", arg)
3247
3248        # We need these casts to make duckdb's static type checker happy
3249        blob = exp.cast(arg, exp.DType.VARBINARY)
3250        varchar = exp.cast(arg, exp.DType.VARCHAR)
3251
3252        case = (
3253            exp.case(exp.Anonymous(this="TYPEOF", expressions=[arg]))
3254            .when(exp.Literal.string("BLOB"), exp.ByteLength(this=blob))
3255            .else_(exp.Anonymous(this="LENGTH", expressions=[varchar]))
3256        )
3257        return self.sql(case)
3258
3259    def bitlength_sql(self, expression: exp.BitLength) -> str:
3260        if not _is_binary(arg := expression.this):
3261            return self.func("BIT_LENGTH", arg)
3262
3263        blob = exp.cast(arg, exp.DataType.Type.VARBINARY)
3264        return self.sql(exp.ByteLength(this=blob) * exp.Literal.number(8))
3265
3266    def chr_sql(self, expression: exp.Chr, name: str = "CHR") -> str:
3267        arg = expression.expressions[0]
3268        if arg.is_type(*exp.DataType.REAL_TYPES):
3269            arg = exp.cast(arg, exp.DType.INT)
3270        return self.func("CHR", arg)
3271
3272    def collation_sql(self, expression: exp.Collation) -> str:
3273        self.unsupported("COLLATION function is not supported by DuckDB")
3274        return self.function_fallback_sql(expression)
3275
3276    def collate_sql(self, expression: exp.Collate) -> str:
3277        if not expression.expression.is_string:
3278            return super().collate_sql(expression)
3279
3280        raw = expression.expression.name
3281        if not raw:
3282            return self.sql(expression.this)
3283
3284        parts = []
3285        for part in raw.split("-"):
3286            lower = part.lower()
3287            if lower not in _SNOWFLAKE_COLLATION_DEFAULTS:
3288                if lower in _SNOWFLAKE_COLLATION_UNSUPPORTED:
3289                    self.unsupported(
3290                        f"Snowflake collation specifier '{part}' has no DuckDB equivalent"
3291                    )
3292                parts.append(lower)
3293
3294        if not parts:
3295            return self.sql(expression.this)
3296        return super().collate_sql(
3297            exp.Collate(this=expression.this, expression=exp.var(".".join(parts)))
3298        )
3299
3300    def _validate_regexp_flags(self, flags: exp.Expr | None, supported_flags: str) -> str | None:
3301        """
3302        Validate and filter regexp flags for DuckDB compatibility.
3303
3304        Args:
3305            flags: The flags expression to validate
3306            supported_flags: String of supported flags (e.g., "ims", "cims").
3307                            Only these flags will be returned.
3308
3309        Returns:
3310            Validated/filtered flag string, or None if no valid flags remain
3311        """
3312        if not isinstance(flags, exp.Expr):
3313            return None
3314
3315        if not flags.is_string:
3316            self.unsupported("Non-literal regexp flags are not fully supported in DuckDB")
3317            return None
3318
3319        flag_str = flags.this
3320        unsupported = set(flag_str) - set(supported_flags)
3321
3322        if unsupported:
3323            self.unsupported(
3324                f"Regexp flags {sorted(unsupported)} are not supported in this context"
3325            )
3326
3327        flag_str = "".join(f for f in flag_str if f in supported_flags)
3328        return flag_str if flag_str else None
3329
3330    def regexpcount_sql(self, expression: exp.RegexpCount) -> str:
3331        this = expression.this
3332        pattern = expression.expression
3333        position = expression.args.get("position")
3334        parameters = expression.args.get("parameters")
3335
3336        # Validate flags - only "ims" flags are supported for embedded patterns
3337        validated_flags = self._validate_regexp_flags(parameters, supported_flags="ims")
3338
3339        if position:
3340            this = exp.Substring(this=this, start=position)
3341
3342        # Embed flags in pattern (REGEXP_EXTRACT_ALL doesn't support flags argument)
3343        if validated_flags:
3344            pattern = exp.Concat(expressions=[exp.Literal.string(f"(?{validated_flags})"), pattern])
3345
3346        # Handle empty pattern: Snowflake returns 0, DuckDB would match between every character
3347        result = (
3348            exp.case()
3349            .when(
3350                exp.EQ(this=pattern, expression=exp.Literal.string("")),
3351                exp.Literal.number(0),
3352            )
3353            .else_(
3354                exp.Length(
3355                    this=exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern])
3356                )
3357            )
3358        )
3359
3360        return self.sql(result)
3361
3362    def regexpreplace_sql(self, expression: exp.RegexpReplace) -> str:
3363        subject = expression.this
3364        pattern = expression.expression
3365        replacement = expression.args.get("replacement") or exp.Literal.string("")
3366        position = expression.args.get("position")
3367        occurrence = expression.args.get("occurrence")
3368        modifiers = expression.args.get("modifiers")
3369
3370        validated_flags = self._validate_regexp_flags(modifiers, supported_flags="cimsg") or ""
3371
3372        # Handle occurrence (only literals supported)
3373        if occurrence and not occurrence.is_int:
3374            self.unsupported("REGEXP_REPLACE with non-literal occurrence")
3375        else:
3376            occurrence = occurrence.to_py() if occurrence and occurrence.is_int else 0
3377            if occurrence > 1:
3378                self.unsupported(f"REGEXP_REPLACE occurrence={occurrence} not supported")
3379            # flag duckdb to do either all or none, single_replace check is for duckdb round trip
3380            elif (
3381                occurrence == 0
3382                and "g" not in validated_flags
3383                and not expression.args.get("single_replace")
3384            ):
3385                validated_flags += "g"
3386
3387        # Handle position (only literals supported)
3388        prefix = None
3389        if position and not position.is_int:
3390            self.unsupported("REGEXP_REPLACE with non-literal position")
3391        elif position and position.is_int and position.to_py() > 1:
3392            pos = position.to_py()
3393            prefix = exp.Substring(
3394                this=subject, start=exp.Literal.number(1), length=exp.Literal.number(pos - 1)
3395            )
3396            subject = exp.Substring(this=subject, start=exp.Literal.number(pos))
3397
3398        result: exp.Expr = exp.Anonymous(
3399            this="REGEXP_REPLACE",
3400            expressions=[
3401                subject,
3402                pattern,
3403                replacement,
3404                exp.Literal.string(validated_flags) if validated_flags else None,
3405            ],
3406        )
3407
3408        if prefix:
3409            result = exp.Concat(expressions=[prefix, result])
3410
3411        return self.sql(result)
3412
3413    def regexplike_sql(self, expression: exp.RegexpLike) -> str:
3414        this = expression.this
3415        pattern = expression.expression
3416        flag = expression.args.get("flag")
3417
3418        if expression.args.get("full_match"):
3419            validated_flags = self._validate_regexp_flags(flag, supported_flags="cims")
3420            flag = exp.Literal.string(validated_flags) if validated_flags else None
3421            return self.func("REGEXP_FULL_MATCH", this, pattern, flag)
3422
3423        return self.func("REGEXP_MATCHES", this, pattern, flag)
3424
3425    @unsupported_args("ins_cost", "del_cost", "sub_cost")
3426    def levenshtein_sql(self, expression: exp.Levenshtein) -> str:
3427        this = expression.this
3428        expr = expression.expression
3429        max_dist = expression.args.get("max_dist")
3430
3431        if max_dist is None:
3432            return self.func("LEVENSHTEIN", this, expr)
3433
3434        # Emulate Snowflake semantics: if distance > max_dist, return max_dist
3435        levenshtein = exp.Levenshtein(this=this, expression=expr)
3436        return self.sql(exp.Least(this=levenshtein, expressions=[max_dist]))
3437
3438    def pad_sql(self, expression: exp.Pad) -> str:
3439        """
3440        Handle RPAD/LPAD for VARCHAR and BINARY types.
3441
3442        For VARCHAR: Delegate to parent class
3443        For BINARY: Lower to: input || REPEAT(pad, GREATEST(0, target_len - OCTET_LENGTH(input)))
3444        """
3445        string_arg = expression.this
3446        fill_arg = expression.args.get("fill_pattern") or exp.Literal.string(" ")
3447
3448        if _is_binary(string_arg) or _is_binary(fill_arg):
3449            length_arg = expression.expression
3450            is_left = expression.args.get("is_left")
3451
3452            input_len = exp.ByteLength(this=string_arg)
3453            chars_needed = length_arg - input_len
3454            pad_count = exp.Greatest(
3455                this=exp.Literal.number(0), expressions=[chars_needed], ignore_nulls=True
3456            )
3457            repeat_expr = exp.Repeat(this=fill_arg, times=pad_count)
3458
3459            left, right = string_arg, repeat_expr
3460            if is_left:
3461                left, right = right, left
3462
3463            result = exp.DPipe(this=left, expression=right)
3464            return self.sql(result)
3465
3466        # For VARCHAR: Delegate to parent class (handles PAD_FILL_PATTERN_IS_REQUIRED)
3467        return super().pad_sql(expression)
3468
3469    def minhash_sql(self, expression: exp.Minhash) -> str:
3470        k = expression.this
3471        exprs = expression.expressions
3472
3473        if len(exprs) != 1 or isinstance(exprs[0], exp.Star):
3474            self.unsupported(
3475                "MINHASH with multiple expressions or * requires manual query restructuring"
3476            )
3477            return self.func("MINHASH", k, *exprs)
3478
3479        expr = exprs[0]
3480        result = exp.replace_placeholders(self.MINHASH_TEMPLATE.copy(), expr=expr, k=k)
3481        return f"({self.sql(result)})"
3482
3483    def minhashcombine_sql(self, expression: exp.MinhashCombine) -> str:
3484        expr = expression.this
3485        result = exp.replace_placeholders(self.MINHASH_COMBINE_TEMPLATE.copy(), expr=expr)
3486        return f"({self.sql(result)})"
3487
3488    def approximatesimilarity_sql(self, expression: exp.ApproximateSimilarity) -> str:
3489        expr = expression.this
3490        result = exp.replace_placeholders(self.APPROXIMATE_SIMILARITY_TEMPLATE.copy(), expr=expr)
3491        return f"({self.sql(result)})"
3492
3493    def arrayuniqueagg_sql(self, expression: exp.ArrayUniqueAgg) -> str:
3494        return self.sql(
3495            exp.Filter(
3496                this=exp.func("LIST", exp.Distinct(expressions=[expression.this])),
3497                expression=exp.Where(this=expression.this.copy().is_(exp.null()).not_()),
3498            )
3499        )
3500
3501    def arrayconcatagg_sql(self, expression: exp.ArrayConcatAgg) -> str:
3502        this = expression.this
3503
3504        if isinstance(this, exp.Limit):
3505            self.unsupported("LIMIT in ARRAY_CONCAT_AGG cannot be transpiled to DuckDB")
3506            this = this.this
3507
3508        inner = this.this if isinstance(this, exp.Order) else this
3509
3510        return self.func(
3511            "FLATTEN",
3512            exp.Filter(
3513                this=exp.ArrayAgg(this=this),
3514                expression=exp.Where(this=inner.copy().is_(exp.null()).not_()),
3515            ),
3516        )
3517
3518    def arrayunionagg_sql(self, expression: exp.ArrayUnionAgg) -> str:
3519        self.unsupported("ARRAY_UNION_AGG is not supported in DuckDB")
3520        return self.function_fallback_sql(expression)
3521
3522    def arraydistinct_sql(self, expression: exp.ArrayDistinct) -> str:
3523        arr = expression.this
3524        func = self.func("LIST_DISTINCT", arr)
3525
3526        if expression.args.get("check_null"):
3527            add_null_to_array = exp.func(
3528                "LIST_APPEND", exp.func("LIST_DISTINCT", exp.ArrayCompact(this=arr)), exp.Null()
3529            )
3530            return self.sql(
3531                exp.If(
3532                    this=exp.NEQ(
3533                        this=exp.ArraySize(this=arr), expression=exp.func("LIST_COUNT", arr)
3534                    ),
3535                    true=add_null_to_array,
3536                    false=func,
3537                )
3538            )
3539
3540        return func
3541
3542    def arrayintersect_sql(self, expression: exp.ArrayIntersect) -> str:
3543        if expression.args.get("is_multiset") and len(expression.expressions) == 2:
3544            return self._array_bag_sql(
3545                self.ARRAY_INTERSECTION_CONDITION,
3546                expression.expressions[0],
3547                expression.expressions[1],
3548            )
3549        return self.function_fallback_sql(expression)
3550
3551    def arrayexcept_sql(self, expression: exp.ArrayExcept) -> str:
3552        arr1, arr2 = expression.this, expression.expression
3553        if expression.args.get("is_multiset"):
3554            return self._array_bag_sql(self.ARRAY_EXCEPT_CONDITION, arr1, arr2)
3555        return self.sql(
3556            exp.replace_placeholders(self.ARRAY_EXCEPT_SET_TEMPLATE, arr1=arr1, arr2=arr2)
3557        )
3558
3559    def arrayslice_sql(self, expression: exp.ArraySlice) -> str:
3560        """
3561        Transpiles Snowflake's ARRAY_SLICE (0-indexed, exclusive end) to DuckDB's
3562        ARRAY_SLICE (1-indexed, inclusive end) by wrapping start and end in CASE
3563        expressions that adjust the index at query time:
3564          - start: CASE WHEN start >= 0 THEN start + 1 ELSE start END
3565          - end:   CASE WHEN end < 0 THEN end - 1 ELSE end END
3566        """
3567        start, end = expression.args.get("start"), expression.args.get("end")
3568
3569        if expression.args.get("zero_based"):
3570            if start is not None:
3571                start = (
3572                    exp.case()
3573                    .when(
3574                        exp.GTE(this=start.copy(), expression=exp.Literal.number(0)),
3575                        exp.Add(this=start.copy(), expression=exp.Literal.number(1)),
3576                    )
3577                    .else_(start)
3578                )
3579            if end is not None:
3580                end = (
3581                    exp.case()
3582                    .when(
3583                        exp.LT(this=end.copy(), expression=exp.Literal.number(0)),
3584                        exp.Sub(this=end.copy(), expression=exp.Literal.number(1)),
3585                    )
3586                    .else_(end)
3587                )
3588
3589        return self.func("ARRAY_SLICE", expression.this, start, end, expression.args.get("step"))
3590
3591    def arrayszip_sql(self, expression: exp.ArraysZip) -> str:
3592        args = expression.expressions
3593
3594        if not args:
3595            # Return [{}] - using MAP([], []) since DuckDB can't represent empty structs
3596            return self.sql(exp.array(exp.Map(keys=exp.array(), values=exp.array())))
3597
3598        # Build placeholder values for template
3599        lengths = [exp.Length(this=arg) for arg in args]
3600        max_len = (
3601            lengths[0]
3602            if len(lengths) == 1
3603            else exp.Greatest(this=lengths[0], expressions=lengths[1:])
3604        )
3605
3606        # Empty struct with same schema: {'$1': NULL, '$2': NULL, ...}
3607        empty_struct = exp.func(
3608            "STRUCT",
3609            *[
3610                exp.PropertyEQ(this=exp.Literal.string(f"${i + 1}"), expression=exp.Null())
3611                for i in range(len(args))
3612            ],
3613        )
3614
3615        # Struct for transform: {'$1': COALESCE(arr1, [])[__i + 1], ...}
3616        # COALESCE wrapping handles NULL arrays - prevents invalid NULL[i] syntax
3617        index = exp.column("__i") + 1
3618        transform_struct = exp.func(
3619            "STRUCT",
3620            *[
3621                exp.PropertyEQ(
3622                    this=exp.Literal.string(f"${i + 1}"),
3623                    expression=exp.func("COALESCE", arg, exp.array())[index],
3624                )
3625                for i, arg in enumerate(args)
3626            ],
3627        )
3628
3629        result = exp.replace_placeholders(
3630            self.ARRAYS_ZIP_TEMPLATE.copy(),
3631            null_check=exp.or_(*[arg.is_(exp.Null()) for arg in args]),
3632            all_empty_check=exp.and_(
3633                *[
3634                    exp.EQ(this=exp.Length(this=arg), expression=exp.Literal.number(0))
3635                    for arg in args
3636                ]
3637            ),
3638            empty_struct=empty_struct,
3639            max_len=max_len,
3640            transform_struct=transform_struct,
3641        )
3642        return self.sql(result)
3643
3644    def lower_sql(self, expression: exp.Lower) -> str:
3645        result_sql = self.func("LOWER", _cast_to_varchar(expression.this))
3646        return _gen_with_cast_to_blob(self, expression, result_sql)
3647
3648    def upper_sql(self, expression: exp.Upper) -> str:
3649        result_sql = self.func("UPPER", _cast_to_varchar(expression.this))
3650        return _gen_with_cast_to_blob(self, expression, result_sql)
3651
3652    def reverse_sql(self, expression: exp.Reverse) -> str:
3653        result_sql = self.func("REVERSE", _cast_to_varchar(expression.this))
3654        return _gen_with_cast_to_blob(self, expression, result_sql)
3655
3656    def _left_right_sql(self, expression: exp.Left | exp.Right, func_name: str) -> str:
3657        arg = expression.this
3658        length = expression.expression
3659        is_binary = _is_binary(arg)
3660
3661        if is_binary:
3662            # LEFT/RIGHT(blob, n) becomes UNHEX(LEFT/RIGHT(HEX(blob), n * 2))
3663            # Each byte becomes 2 hex chars, so multiply length by 2
3664            hex_arg = exp.Hex(this=arg)
3665            hex_length = exp.Mul(this=length, expression=exp.Literal.number(2))
3666            result: exp.Expression = exp.Unhex(
3667                this=exp.Anonymous(this=func_name, expressions=[hex_arg, hex_length])
3668            )
3669        else:
3670            result = exp.Anonymous(this=func_name, expressions=[arg, length])
3671
3672        if expression.args.get("negative_length_returns_empty"):
3673            empty: exp.Expression = exp.Literal.string("")
3674            if is_binary:
3675                empty = exp.Unhex(this=empty)
3676            result = exp.case().when(length < exp.Literal.number(0), empty).else_(result)
3677
3678        return self.sql(result)
3679
3680    def left_sql(self, expression: exp.Left) -> str:
3681        return self._left_right_sql(expression, "LEFT")
3682
3683    def right_sql(self, expression: exp.Right) -> str:
3684        return self._left_right_sql(expression, "RIGHT")
3685
3686    def rtrimmedlength_sql(self, expression: exp.RtrimmedLength) -> str:
3687        return self.func("LENGTH", exp.Trim(this=expression.this, position="TRAILING"))
3688
3689    def stuff_sql(self, expression: exp.Stuff) -> str:
3690        base = expression.this
3691        start = expression.args["start"]
3692        length = expression.args["length"]
3693        insertion = expression.expression
3694        is_binary = _is_binary(base)
3695
3696        if is_binary:
3697            # DuckDB's SUBSTRING doesn't accept BLOB; operate on the HEX string instead
3698            # (each byte = 2 hex chars), then UNHEX back to BLOB
3699            base = exp.Hex(this=base)
3700            insertion = exp.Hex(this=insertion)
3701            left = exp.Substring(
3702                this=base.copy(),
3703                start=exp.Literal.number(1),
3704                length=(start.copy() - exp.Literal.number(1)) * exp.Literal.number(2),
3705            )
3706            right = exp.Substring(
3707                this=base.copy(),
3708                start=((start + length) - exp.Literal.number(1)) * exp.Literal.number(2)
3709                + exp.Literal.number(1),
3710            )
3711        else:
3712            left = exp.Substring(
3713                this=base.copy(),
3714                start=exp.Literal.number(1),
3715                length=start.copy() - exp.Literal.number(1),
3716            )
3717            right = exp.Substring(this=base.copy(), start=start + length)
3718        result: exp.Expr = exp.DPipe(
3719            this=exp.DPipe(this=left, expression=insertion), expression=right
3720        )
3721
3722        if is_binary:
3723            result = exp.Unhex(this=result)
3724
3725        return self.sql(result)
3726
3727    def rand_sql(self, expression: exp.Rand) -> str:
3728        seed = expression.this
3729        if seed is not None:
3730            self.unsupported("RANDOM with seed is not supported in DuckDB")
3731
3732        lower = expression.args.get("lower")
3733        upper = expression.args.get("upper")
3734
3735        if lower and upper:
3736            # scale DuckDB's [0,1) to the specified range
3737            range_size = exp.paren(upper - lower)
3738            scaled = exp.Add(this=lower, expression=exp.func("random") * range_size)
3739
3740            # For now we assume that if bounds are set, return type is BIGINT. Snowflake/Teradata
3741            result = exp.cast(scaled, exp.DType.BIGINT)
3742            return self.sql(result)
3743
3744        # Default DuckDB behavior - just return RANDOM() as float
3745        return "RANDOM()"
3746
3747    def bytelength_sql(self, expression: exp.ByteLength) -> str:
3748        arg = expression.this
3749
3750        # Check if it's a text type (handles both literals and annotated expressions)
3751        if arg.is_type(*exp.DataType.TEXT_TYPES):
3752            return self.func("OCTET_LENGTH", exp.Encode(this=arg))
3753
3754        # Default: pass through as-is (conservative for DuckDB, handles binary and unannotated)
3755        return self.func("OCTET_LENGTH", arg)
3756
3757    def base64encode_sql(self, expression: exp.Base64Encode) -> str:
3758        # DuckDB TO_BASE64 requires BLOB input
3759        # Snowflake BASE64_ENCODE accepts both VARCHAR and BINARY - for VARCHAR it implicitly
3760        # encodes UTF-8 bytes. We add ENCODE unless the input is a binary type.
3761        result = expression.this
3762
3763        # Check if input is a string type - ENCODE only accepts VARCHAR
3764        if result.is_type(*exp.DataType.TEXT_TYPES):
3765            result = exp.Encode(this=result)
3766
3767        result = exp.ToBase64(this=result)
3768
3769        max_line_length = expression.args.get("max_line_length")
3770        alphabet = expression.args.get("alphabet")
3771
3772        # Handle custom alphabet by replacing standard chars with custom ones
3773        result = _apply_base64_alphabet_replacements(result, alphabet)
3774
3775        # Handle max_line_length by inserting newlines every N characters
3776        line_length = (
3777            t.cast(int, max_line_length.to_py())
3778            if isinstance(max_line_length, exp.Literal) and max_line_length.is_number
3779            else 0
3780        )
3781        if line_length > 0:
3782            newline = exp.Chr(expressions=[exp.Literal.number(10)])
3783            result = exp.Trim(
3784                this=exp.RegexpReplace(
3785                    this=result,
3786                    expression=exp.Literal.string(f"(.{{{line_length}}})"),
3787                    replacement=exp.Concat(expressions=[exp.Literal.string("\\1"), newline.copy()]),
3788                ),
3789                expression=newline,
3790                position="TRAILING",
3791            )
3792
3793        return self.sql(result)
3794
3795    def hex_sql(self, expression: exp.Hex) -> str:
3796        case = expression.args.get("case")
3797
3798        if not case:
3799            return self.func("HEX", expression.this)
3800
3801        hex_expr = exp.Hex(this=expression.this)
3802        return self.sql(
3803            exp.case()
3804            .when(case.is_(exp.null()), exp.null())
3805            .when(case.copy().eq(0), exp.Lower(this=hex_expr.copy()))
3806            .else_(hex_expr)
3807        )
3808
3809    def replace_sql(self, expression: exp.Replace) -> str:
3810        result_sql = self.func(
3811            "REPLACE",
3812            _cast_to_varchar(expression.this),
3813            _cast_to_varchar(expression.expression),
3814            _cast_to_varchar(expression.args.get("replacement")),
3815        )
3816        return _gen_with_cast_to_blob(self, expression, result_sql)
3817
3818    def _bitwise_op(self, expression: exp.Binary, op: str) -> str:
3819        _prepare_binary_bitwise_args(expression)
3820        result_sql = self.binary(expression, op)
3821        return _gen_with_cast_to_blob(self, expression, result_sql)
3822
3823    def bitwisexor_sql(self, expression: exp.BitwiseXor) -> str:
3824        _prepare_binary_bitwise_args(expression)
3825        result_sql = self.func("XOR", expression.this, expression.expression)
3826        return _gen_with_cast_to_blob(self, expression, result_sql)
3827
3828    def objectinsert_sql(self, expression: exp.ObjectInsert) -> str:
3829        this = expression.this
3830        key = expression.args.get("key")
3831        key_sql = key.name if isinstance(key, exp.Expr) else ""
3832        value_sql = self.sql(expression, "value")
3833
3834        kv_sql = f"{key_sql} := {value_sql}"
3835
3836        # If the input struct is empty e.g. transpiling OBJECT_INSERT(OBJECT_CONSTRUCT(), key, value) from Snowflake
3837        # then we can generate STRUCT_PACK which will build it since STRUCT_INSERT({}, key := value) is not valid DuckDB
3838        if isinstance(this, exp.Struct) and not this.expressions:
3839            return self.func("STRUCT_PACK", kv_sql)
3840
3841        return self.func("STRUCT_INSERT", this, kv_sql)
3842
3843    def mapcat_sql(self, expression: exp.MapCat) -> str:
3844        result = exp.replace_placeholders(
3845            self.MAPCAT_TEMPLATE.copy(),
3846            map1=expression.this,
3847            map2=expression.expression,
3848        )
3849        return self.sql(result)
3850
3851    def mapcontainskey_sql(self, expression: exp.MapContainsKey) -> str:
3852        return self.func(
3853            "ARRAY_CONTAINS", exp.func("MAP_KEYS", expression.args["key"]), expression.this
3854        )
3855
3856    def mapdelete_sql(self, expression: exp.MapDelete) -> str:
3857        map_arg = expression.this
3858        keys_to_delete = expression.expressions
3859
3860        x_dot_key = exp.Dot(this=exp.to_identifier("x"), expression=exp.to_identifier("key"))
3861
3862        lambda_expr = exp.Lambda(
3863            this=exp.In(this=x_dot_key, expressions=keys_to_delete).not_(),
3864            expressions=[exp.to_identifier("x")],
3865        )
3866        result = exp.func(
3867            "MAP_FROM_ENTRIES",
3868            exp.ArrayFilter(this=exp.func("MAP_ENTRIES", map_arg), expression=lambda_expr),
3869        )
3870        return self.sql(result)
3871
3872    def mappick_sql(self, expression: exp.MapPick) -> str:
3873        map_arg = expression.this
3874        keys_to_pick = expression.expressions
3875
3876        x_dot_key = exp.Dot(this=exp.to_identifier("x"), expression=exp.to_identifier("key"))
3877
3878        if len(keys_to_pick) == 1 and keys_to_pick[0].is_type(exp.DType.ARRAY):
3879            lambda_expr = exp.Lambda(
3880                this=exp.func("ARRAY_CONTAINS", keys_to_pick[0], x_dot_key),
3881                expressions=[exp.to_identifier("x")],
3882            )
3883        else:
3884            lambda_expr = exp.Lambda(
3885                this=exp.In(this=x_dot_key, expressions=keys_to_pick),
3886                expressions=[exp.to_identifier("x")],
3887            )
3888
3889        result = exp.func(
3890            "MAP_FROM_ENTRIES",
3891            exp.func("LIST_FILTER", exp.func("MAP_ENTRIES", map_arg), lambda_expr),
3892        )
3893        return self.sql(result)
3894
3895    def mapsize_sql(self, expression: exp.MapSize) -> str:
3896        return self.func("CARDINALITY", expression.this)
3897
3898    @unsupported_args("update_flag")
3899    def mapinsert_sql(self, expression: exp.MapInsert) -> str:
3900        map_arg = expression.this
3901        key = expression.args.get("key")
3902        value = expression.args.get("value")
3903
3904        map_type = map_arg.type
3905
3906        if value is not None:
3907            if map_type and map_type.expressions and len(map_type.expressions) > 1:
3908                # Extract the value type from MAP(key_type, value_type)
3909                value_type = map_type.expressions[1]
3910                # Cast value to match the map's value type to avoid type conflicts
3911                value = exp.cast(value, value_type)
3912            # else: polymorphic MAP case - no type parameters available, use value as-is
3913
3914        # Create a single-entry map for the new key-value pair
3915        new_entry_struct = exp.Struct(expressions=[exp.PropertyEQ(this=key, expression=value)])
3916        new_entry: exp.Expression = exp.ToMap(this=new_entry_struct)
3917
3918        # Use MAP_CONCAT to merge the original map with the new entry
3919        # This automatically handles both insert and update cases
3920        result = exp.func("MAP_CONCAT", map_arg, new_entry)
3921
3922        return self.sql(result)
3923
3924    def startswith_sql(self, expression: exp.StartsWith) -> str:
3925        return self.func(
3926            "STARTS_WITH",
3927            _cast_to_varchar(expression.this),
3928            _cast_to_varchar(expression.expression),
3929        )
3930
3931    def space_sql(self, expression: exp.Space) -> str:
3932        # DuckDB's REPEAT requires BIGINT for the count parameter
3933        return self.sql(
3934            exp.Repeat(
3935                this=exp.Literal.string(" "),
3936                times=exp.cast(expression.this, exp.DType.BIGINT),
3937            )
3938        )
3939
3940    def tablefromrows_sql(self, expression: exp.TableFromRows) -> str:
3941        # For GENERATOR, unwrap TABLE() - just emit the Generator (becomes RANGE)
3942        if isinstance(expression.this, exp.Generator):
3943            # Preserve alias, joins, and other table-level args
3944            table = exp.Table(
3945                this=expression.this,
3946                alias=expression.args.get("alias"),
3947                joins=expression.args.get("joins"),
3948            )
3949            return self.sql(table)
3950
3951        return super().tablefromrows_sql(expression)
3952
3953    def unnest_sql(self, expression: exp.Unnest) -> str:
3954        explode_array = expression.args.get("explode_array")
3955        if explode_array:
3956            # In BigQuery, UNNESTing a nested array leads to explosion of the top-level array & struct
3957            # This is transpiled to DDB by transforming "FROM UNNEST(...)" to "FROM (SELECT UNNEST(..., max_depth => 2))"
3958            expression.expressions.append(
3959                exp.Kwarg(this=exp.var("max_depth"), expression=exp.Literal.number(2))
3960            )
3961
3962            # If BQ's UNNEST is aliased, we transform it from a column alias to a table alias in DDB
3963            alias = expression.args.get("alias")
3964            if isinstance(alias, exp.TableAlias):
3965                expression.set("alias", None)
3966                if alias.columns:
3967                    alias = exp.TableAlias(this=seq_get(alias.columns, 0))
3968
3969            unnest_sql = super().unnest_sql(expression)
3970            select = exp.Select(expressions=[unnest_sql]).subquery(alias)
3971            return self.sql(select)
3972
3973        return super().unnest_sql(expression)
3974
3975    def arrayagg_sql(self, expression: exp.ArrayAgg) -> str:
3976        if isinstance(expression.this, exp.Limit):
3977            self.unsupported("LIMIT inside ARRAY_AGG is not supported in DuckDB")
3978
3979        return super().arrayagg_sql(expression)
3980
3981    def ignorenulls_sql(self, expression: exp.IgnoreNulls) -> str:
3982        this = expression.this
3983
3984        if isinstance(this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
3985            # DuckDB should render IGNORE NULLS only for the general-purpose
3986            # window functions that accept it e.g. FIRST_VALUE(... IGNORE NULLS) OVER (...)
3987            return super().ignorenulls_sql(expression)
3988
3989        # For ARRAY_AGG(expr IGNORE NULLS ...), convert IGNORE NULLS to a
3990        # FILTER(WHERE expr IS NOT NULL) clause by setting nulls_excluded on
3991        # the ArrayAgg.  The existing _add_arrayagg_null_filter method will
3992        # emit the FILTER clause during arrayagg_sql / withingroup_sql.
3993        if isinstance(this, exp.ArrayAgg):
3994            this.set("nulls_excluded", True)
3995            return self.sql(this)
3996
3997        if isinstance(this, exp.First):
3998            this = exp.AnyValue(this=this.this)
3999
4000        if not isinstance(this, (exp.AnyValue, exp.ApproxQuantiles)):
4001            self.unsupported("IGNORE NULLS is not supported for non-window functions.")
4002
4003        return self.sql(this)
4004
4005    def split_sql(self, expression: exp.Split) -> str:
4006        base_func = exp.func("STR_SPLIT", expression.this, expression.expression)
4007
4008        case_expr = exp.case().else_(base_func)
4009        needs_case = False
4010
4011        if expression.args.get("null_returns_null"):
4012            case_expr = case_expr.when(expression.expression.is_(exp.null()), exp.null())
4013            needs_case = True
4014
4015        if expression.args.get("empty_delimiter_returns_whole"):
4016            # When delimiter is empty string, return input string as single array element
4017            array_with_input = exp.array(expression.this)
4018            case_expr = case_expr.when(
4019                expression.expression.eq(exp.Literal.string("")), array_with_input
4020            )
4021            needs_case = True
4022
4023        return self.sql(case_expr if needs_case else base_func)
4024
4025    def splitpart_sql(self, expression: exp.SplitPart) -> str:
4026        string_arg = expression.this
4027        delimiter_arg = expression.args.get("delimiter")
4028        part_index_arg = expression.args.get("part_index")
4029
4030        if delimiter_arg and part_index_arg:
4031            # Handle Snowflake's "index 0 and 1 both return first element" behavior
4032            if expression.args.get("part_index_zero_as_one"):
4033                # Convert 0 to 1 for compatibility
4034
4035                part_index_arg = exp.Paren(
4036                    this=exp.case()
4037                    .when(part_index_arg.eq(exp.Literal.number("0")), exp.Literal.number("1"))
4038                    .else_(part_index_arg)
4039                )
4040
4041            # Use Anonymous to avoid recursion
4042            base_func_expr: exp.Expr = exp.Anonymous(
4043                this="SPLIT_PART", expressions=[string_arg, delimiter_arg, part_index_arg]
4044            )
4045            needs_case_transform = False
4046            case_expr = exp.case().else_(base_func_expr)
4047
4048            if expression.args.get("empty_delimiter_returns_whole"):
4049                # When delimiter is empty string:
4050                # - Return whole string if part_index is 1 or -1
4051                # - Return empty string otherwise
4052                empty_case = exp.Paren(
4053                    this=exp.case()
4054                    .when(
4055                        exp.or_(
4056                            part_index_arg.eq(exp.Literal.number("1")),
4057                            part_index_arg.eq(exp.Literal.number("-1")),
4058                        ),
4059                        string_arg,
4060                    )
4061                    .else_(exp.Literal.string(""))
4062                )
4063
4064                case_expr = case_expr.when(delimiter_arg.eq(exp.Literal.string("")), empty_case)
4065                needs_case_transform = True
4066
4067            """
4068            Output looks something like this:
4069
4070            CASE
4071            WHEN delimiter is '' THEN
4072                (
4073                    CASE
4074                    WHEN adjusted_part_index = 1 OR adjusted_part_index = -1 THEN input
4075                    ELSE '' END
4076                )
4077            ELSE SPLIT_PART(input, delimiter, adjusted_part_index)
4078            END
4079
4080            """
4081            return self.sql(case_expr if needs_case_transform else base_func_expr)
4082
4083        return self.function_fallback_sql(expression)
4084
4085    def respectnulls_sql(self, expression: exp.RespectNulls) -> str:
4086        if isinstance(expression.this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
4087            # DuckDB should render RESPECT NULLS only for the general-purpose
4088            # window functions that accept it e.g. FIRST_VALUE(... RESPECT NULLS) OVER (...)
4089            return super().respectnulls_sql(expression)
4090
4091        self.unsupported("RESPECT NULLS is not supported for non-window functions.")
4092        return self.sql(expression, "this")
4093
4094    def arraytostring_sql(self, expression: exp.ArrayToString) -> str:
4095        null = expression.args.get("null")
4096
4097        if expression.args.get("null_is_empty"):
4098            x = exp.to_identifier("x")
4099            list_transform = exp.Transform(
4100                this=expression.this.copy(),
4101                expression=exp.Lambda(
4102                    this=exp.Coalesce(
4103                        this=exp.cast(x, "TEXT"), expressions=[exp.Literal.string("")]
4104                    ),
4105                    expressions=[x],
4106                ),
4107            )
4108            array_to_string = exp.ArrayToString(
4109                this=list_transform, expression=expression.expression
4110            )
4111            if expression.args.get("null_delim_is_null"):
4112                return self.sql(
4113                    exp.case()
4114                    .when(expression.expression.copy().is_(exp.null()), exp.null())
4115                    .else_(array_to_string)
4116                )
4117            return self.sql(array_to_string)
4118
4119        if null:
4120            x = exp.to_identifier("x")
4121            return self.sql(
4122                exp.ArrayToString(
4123                    this=exp.Transform(
4124                        this=expression.this,
4125                        expression=exp.Lambda(
4126                            this=exp.Coalesce(this=x, expressions=[null]),
4127                            expressions=[x],
4128                        ),
4129                    ),
4130                    expression=expression.expression,
4131                )
4132            )
4133
4134        return self.func("ARRAY_TO_STRING", expression.this, expression.expression)
4135
4136    def concatws_sql(self, expression: exp.ConcatWs) -> str:
4137        # DuckDB-specific: handle binary types using DPipe (||) operator
4138        separator = seq_get(expression.expressions, 0)
4139        args = expression.expressions[1:]
4140
4141        if any(_is_binary(arg) for arg in [separator, *args]):
4142            result = args[0]
4143            for arg in args[1:]:
4144                result = exp.DPipe(
4145                    this=exp.DPipe(this=result, expression=separator), expression=arg
4146                )
4147            return self.sql(result)
4148
4149        return super().concatws_sql(expression)
4150
4151    def _regexp_extract_sql(self, expression: exp.RegexpExtract | exp.RegexpExtractAll) -> str:
4152        this = expression.this
4153        group = expression.args.get("group")
4154        params = expression.args.get("parameters")
4155        position = expression.args.get("position")
4156        occurrence = expression.args.get("occurrence")
4157        null_if_pos_overflow = expression.args.get("null_if_pos_overflow")
4158
4159        # Handle Snowflake's 'e' flag: it enables capture group extraction
4160        # In DuckDB, this is controlled by the group parameter directly
4161        if params and params.is_string and "e" in params.name:
4162            params = exp.Literal.string(params.name.replace("e", ""))
4163
4164        validated_flags = self._validate_regexp_flags(params, supported_flags="cims")
4165
4166        # Strip default group when no following params (DuckDB default is same as group=0)
4167        if (
4168            not validated_flags
4169            and group
4170            and group.name == str(self.dialect.REGEXP_EXTRACT_DEFAULT_GROUP)
4171        ):
4172            group = None
4173
4174        flags_expr = exp.Literal.string(validated_flags) if validated_flags else None
4175
4176        # use substring to handle position argument
4177        if position and (not position.is_int or position.to_py() > 1):
4178            this = exp.Substring(this=this, start=position)
4179
4180            if null_if_pos_overflow:
4181                this = exp.Nullif(this=this, expression=exp.Literal.string(""))
4182
4183        is_extract_all = isinstance(expression, exp.RegexpExtractAll)
4184        non_single_occurrence = occurrence and (not occurrence.is_int or occurrence.to_py() > 1)
4185
4186        if is_extract_all or non_single_occurrence:
4187            name = "REGEXP_EXTRACT_ALL"
4188        else:
4189            name = "REGEXP_EXTRACT"
4190
4191        result: exp.Expr = exp.Anonymous(
4192            this=name, expressions=[this, expression.expression, group, flags_expr]
4193        )
4194
4195        # Array slicing for REGEXP_EXTRACT_ALL with occurrence
4196        if is_extract_all and non_single_occurrence:
4197            result = exp.Bracket(this=result, expressions=[exp.Slice(this=occurrence)])
4198        # ARRAY_EXTRACT for REGEXP_EXTRACT with occurrence > 1
4199        elif non_single_occurrence:
4200            result = exp.Anonymous(this="ARRAY_EXTRACT", expressions=[result, occurrence])
4201
4202        return self.sql(result)
4203
4204    def regexpextract_sql(self, expression: exp.RegexpExtract) -> str:
4205        return self._regexp_extract_sql(expression)
4206
4207    def regexpextractall_sql(self, expression: exp.RegexpExtractAll) -> str:
4208        return self._regexp_extract_sql(expression)
4209
4210    def regexpinstr_sql(self, expression: exp.RegexpInstr) -> str:
4211        this = expression.this
4212        pattern = expression.expression
4213        position = expression.args.get("position")
4214        orig_occ = expression.args.get("occurrence")
4215        occurrence = orig_occ or exp.Literal.number(1)
4216        option = expression.args.get("option")
4217        parameters = expression.args.get("parameters")
4218
4219        validated_flags = self._validate_regexp_flags(parameters, supported_flags="ims")
4220        if validated_flags:
4221            pattern = exp.Concat(expressions=[exp.Literal.string(f"(?{validated_flags})"), pattern])
4222
4223        # Handle starting position offset
4224        pos_offset: exp.Expr = exp.Literal.number(0)
4225        if position and (not position.is_int or position.to_py() > 1):
4226            this = exp.Substring(this=this, start=position)
4227            pos_offset = position - exp.Literal.number(1)
4228
4229        # Helper: LIST_SUM(LIST_TRANSFORM(list[1:end], x -> LENGTH(x)))
4230        def sum_lengths(func_name: str, end: exp.Expr) -> exp.Expr:
4231            lst = exp.Bracket(
4232                this=exp.Anonymous(this=func_name, expressions=[this, pattern]),
4233                expressions=[exp.Slice(this=exp.Literal.number(1), expression=end)],
4234                offset=1,
4235            )
4236            transform = exp.Anonymous(
4237                this="LIST_TRANSFORM",
4238                expressions=[
4239                    lst,
4240                    exp.Lambda(
4241                        this=exp.Length(this=exp.to_identifier("x")),
4242                        expressions=[exp.to_identifier("x")],
4243                    ),
4244                ],
4245            )
4246            return exp.Coalesce(
4247                this=exp.Anonymous(this="LIST_SUM", expressions=[transform]),
4248                expressions=[exp.Literal.number(0)],
4249            )
4250
4251        # Position = 1 + sum(split_lengths[1:occ]) + sum(match_lengths[1:occ-1]) + offset
4252        base_pos: exp.Expr = (
4253            exp.Literal.number(1)
4254            + sum_lengths("STRING_SPLIT_REGEX", occurrence)
4255            + sum_lengths("REGEXP_EXTRACT_ALL", occurrence - exp.Literal.number(1))
4256            + pos_offset
4257        )
4258
4259        # option=1: add match length for end position
4260        if option and option.is_int and option.to_py() == 1:
4261            match_at_occ = exp.Bracket(
4262                this=exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern]),
4263                expressions=[occurrence],
4264                offset=1,
4265            )
4266            base_pos = base_pos + exp.Coalesce(
4267                this=exp.Length(this=match_at_occ), expressions=[exp.Literal.number(0)]
4268            )
4269
4270        # NULL checks for all provided arguments
4271        # .copy() is used strictly because .is_() alters the node's parent pointer, mutating the parsed AST
4272        null_args = [
4273            expression.this,
4274            expression.expression,
4275            position,
4276            orig_occ,
4277            option,
4278            parameters,
4279        ]
4280        null_checks = [arg.copy().is_(exp.Null()) for arg in null_args if arg]
4281
4282        matches = exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern])
4283
4284        return self.sql(
4285            exp.case()
4286            .when(exp.or_(*null_checks), exp.Null())
4287            .when(pattern.copy().eq(exp.Literal.string("")), exp.Literal.number(0))
4288            .when(exp.Length(this=matches) < occurrence, exp.Literal.number(0))
4289            .else_(base_pos)
4290        )
4291
4292    @unsupported_args("culture")
4293    def numbertostr_sql(self, expression: exp.NumberToStr) -> str:
4294        fmt = expression.args.get("format")
4295        if fmt and fmt.is_int:
4296            return self.func("FORMAT", f"'{{:,.{fmt.name}f}}'", expression.this)
4297
4298        self.unsupported("Only integer formats are supported by NumberToStr")
4299        return self.function_fallback_sql(expression)
4300
4301    def autoincrementcolumnconstraint_sql(self, _) -> str:
4302        self.unsupported("The AUTOINCREMENT column constraint is not supported by DuckDB")
4303        return ""
4304
4305    def aliases_sql(self, expression: exp.Aliases) -> str:
4306        this = expression.this
4307        if isinstance(this, exp.Posexplode):
4308            return self.posexplode_sql(this)
4309
4310        return super().aliases_sql(expression)
4311
4312    def posexplode_sql(self, expression: exp.Posexplode) -> str:
4313        this = expression.this
4314        parent = expression.parent
4315
4316        # The default Spark aliases are "pos" and "col", unless specified otherwise
4317        pos, col = exp.to_identifier("pos"), exp.to_identifier("col")
4318
4319        if isinstance(parent, exp.Aliases):
4320            # Column case: SELECT POSEXPLODE(col) [AS (a, b)]
4321            pos, col = parent.expressions
4322        elif isinstance(parent, exp.Table):
4323            # Table case: SELECT * FROM POSEXPLODE(col) [AS (a, b)]
4324            alias = parent.args.get("alias")
4325            if alias:
4326                pos, col = alias.columns or [pos, col]
4327                alias.pop()
4328
4329        # Translate POSEXPLODE to UNNEST + GENERATE_SUBSCRIPTS
4330        # Note: In Spark pos is 0-indexed, but in DuckDB it's 1-indexed, so we subtract 1 from GENERATE_SUBSCRIPTS
4331        unnest_sql = self.sql(exp.Unnest(expressions=[this], alias=col))
4332        gen_subscripts = self.sql(
4333            exp.Alias(
4334                this=exp.Anonymous(
4335                    this="GENERATE_SUBSCRIPTS", expressions=[this, exp.Literal.number(1)]
4336                )
4337                - exp.Literal.number(1),
4338                alias=pos,
4339            )
4340        )
4341
4342        posexplode_sql = self.format_args(gen_subscripts, unnest_sql)
4343
4344        if isinstance(parent, exp.From) or (parent and isinstance(parent.parent, exp.From)):
4345            # SELECT * FROM POSEXPLODE(col) -> SELECT * FROM (SELECT GENERATE_SUBSCRIPTS(...), UNNEST(...))
4346            return self.sql(exp.Subquery(this=exp.Select(expressions=[posexplode_sql])))
4347
4348        return posexplode_sql
4349
4350    def addmonths_sql(self, expression: exp.AddMonths) -> str:
4351        """
4352        Handles three key issues:
4353        1. Float/decimal months: e.g., Snowflake rounds, whereas DuckDB INTERVAL requires integers
4354        2. End-of-month preservation: If input is last day of month, result is last day of result month
4355        3. Type preservation: Maintains DATE/TIMESTAMPTZ types (DuckDB defaults to TIMESTAMP)
4356        """
4357        from sqlglot.optimizer.annotate_types import annotate_types
4358
4359        this = expression.this
4360        if not this.type:
4361            this = annotate_types(this, dialect=self.dialect)
4362
4363        if this.is_type(*exp.DataType.TEXT_TYPES):
4364            this = exp.Cast(this=this, to=exp.DataType(this=exp.DType.TIMESTAMP))
4365
4366        # Detect float/decimal months to apply rounding (Snowflake behavior)
4367        # DuckDB INTERVAL syntax doesn't support non-integer expressions, so use TO_MONTHS
4368        months_expr = expression.expression
4369        if not months_expr.type:
4370            months_expr = annotate_types(months_expr, dialect=self.dialect)
4371
4372        # Build interval or to_months expression based on type
4373        # Float/decimal case: Round and use TO_MONTHS(CAST(ROUND(value) AS INT))
4374        interval_or_to_months = (
4375            exp.func("TO_MONTHS", exp.cast(exp.func("ROUND", months_expr), "INT"))
4376            if months_expr.is_type(
4377                exp.DType.FLOAT,
4378                exp.DType.DOUBLE,
4379                exp.DType.DECIMAL,
4380            )
4381            # Integer case: standard INTERVAL N MONTH syntax
4382            else exp.Interval(this=months_expr, unit=exp.var("MONTH"))
4383        )
4384
4385        date_add_expr = exp.Add(this=this, expression=interval_or_to_months)
4386
4387        # Apply end-of-month preservation if Snowflake flag is set
4388        # CASE WHEN LAST_DAY(date) = date THEN LAST_DAY(result) ELSE result END
4389        preserve_eom = expression.args.get("preserve_end_of_month")
4390        result_expr = (
4391            exp.case()
4392            .when(
4393                exp.EQ(this=exp.func("LAST_DAY", this), expression=this),
4394                exp.func("LAST_DAY", date_add_expr),
4395            )
4396            .else_(date_add_expr)
4397            if preserve_eom
4398            else date_add_expr
4399        )
4400
4401        # DuckDB's DATE_ADD function returns TIMESTAMP/DATETIME by default, even when the input is DATE
4402        # To match for example Snowflake's ADD_MONTHS behavior (which preserves the input type)
4403        # We need to cast the result back to the original type when the input is DATE or TIMESTAMPTZ
4404        # Example: ADD_MONTHS('2023-01-31'::date, 1) should return DATE, not TIMESTAMP
4405        if this.is_type(exp.DType.DATE, exp.DType.TIMESTAMPTZ):
4406            return self.sql(exp.Cast(this=result_expr, to=this.type))
4407        return self.sql(result_expr)
4408
4409    def format_sql(self, expression: exp.Format) -> str:
4410        if expression.name.lower() == "%s" and len(expression.expressions) == 1:
4411            return self.func("FORMAT", "'{}'", expression.expressions[0])
4412
4413        return self.function_fallback_sql(expression)
4414
4415    def hexstring_sql(
4416        self, expression: exp.HexString, binary_function_repr: str | None = None
4417    ) -> str:
4418        # UNHEX('FF') correctly produces blob \xFF in DuckDB
4419        return super().hexstring_sql(expression, binary_function_repr="UNHEX")
4420
4421    def datetrunc_sql(self, expression: exp.DateTrunc) -> str:
4422        unit = expression.args.get("unit")
4423        date = expression.this
4424
4425        week_start = _week_trunc_start_dow(unit)
4426        unit = unit_to_str(expression)
4427
4428        if week_start:
4429            result = self.sql(
4430                _build_week_trunc_expression(date, week_start, preserve_start_day=True)
4431            )
4432        else:
4433            result = self.func("DATE_TRUNC", unit, date)
4434
4435        if (
4436            expression.args.get("input_type_preserved")
4437            and date.is_type(*exp.DataType.TEMPORAL_TYPES)
4438            and not (is_date_unit(unit) and date.is_type(exp.DType.DATE))
4439        ):
4440            return self.sql(exp.Cast(this=result, to=date.type))
4441
4442        return result
4443
4444    def datetimetrunc_sql(self, expression: exp.DatetimeTrunc) -> str:
4445        this = exp.cast(expression.this, exp.DType.DATETIME)
4446        week_start = _week_trunc_start_dow(expression.args.get("unit"))
4447        if week_start:
4448            return self.sql(
4449                _build_week_trunc_expression(
4450                    this, week_start, preserve_start_day=True, cast_to_date=False
4451                )
4452            )
4453
4454        return self.func("DATE_TRUNC", unit_to_str(expression), this)
4455
4456    def timestamptrunc_sql(self, expression: exp.TimestampTrunc) -> str:
4457        zone = expression.args.get("zone")
4458        timestamp = expression.this
4459        week_start = _week_trunc_start_dow(expression.args.get("unit"))
4460
4461        # The week start emulation below is exact, so avoid weekstart_unit_to_str's degrade warning
4462        unit = unit_to_str(expression) if week_start else weekstart_unit_to_str(self, expression)
4463        date_unit = is_date_unit(unit) or bool(week_start)
4464
4465        def _trunc_expr(this: exp.Expr) -> exp.Expr:
4466            if week_start:
4467                return _build_week_trunc_expression(
4468                    this, week_start, preserve_start_day=True, cast_to_date=False
4469                )
4470            return exp.func("DATE_TRUNC", unit, this)
4471
4472        if date_unit and zone:
4473            # BigQuery's TIMESTAMP_TRUNC with timezone truncates in the target timezone and returns as UTC.
4474            # Double AT TIME ZONE needed for BigQuery compatibility:
4475            # 1. First AT TIME ZONE: ensures truncation happens in the target timezone
4476            # 2. Second AT TIME ZONE: converts the DATE result back to TIMESTAMPTZ (preserving time component)
4477            timestamp = exp.AtTimeZone(this=timestamp, zone=zone)
4478            trunced = _trunc_expr(timestamp)
4479            if isinstance(trunced, exp.DateAdd):
4480                # Parenthesize so the trailing AT TIME ZONE binds to the whole shifted expression
4481                trunced = exp.Paren(this=trunced)
4482            return self.sql(exp.AtTimeZone(this=trunced, zone=zone))
4483
4484        result = self.sql(_trunc_expr(timestamp))
4485        if expression.args.get("input_type_preserved"):
4486            if timestamp.type and timestamp.is_type(exp.DType.TIME, exp.DType.TIMETZ):
4487                dummy_date = exp.Cast(
4488                    this=exp.Literal.string("1970-01-01"),
4489                    to=exp.DataType(this=exp.DType.DATE),
4490                )
4491                date_time = exp.Add(this=dummy_date, expression=timestamp)
4492                result = self.func("DATE_TRUNC", unit, date_time)
4493                return self.sql(exp.Cast(this=result, to=timestamp.type))
4494
4495            if timestamp.is_type(*exp.DataType.TEMPORAL_TYPES) and not (
4496                date_unit and timestamp.is_type(exp.DType.DATE)
4497            ):
4498                return self.sql(exp.Cast(this=result, to=timestamp.type))
4499
4500        return result
4501
4502    def trim_sql(self, expression: exp.Trim) -> str:
4503        expression.this.replace(_cast_to_varchar(expression.this))
4504        if expression.expression:
4505            expression.expression.replace(_cast_to_varchar(expression.expression))
4506
4507        result_sql = super().trim_sql(expression)
4508        return _gen_with_cast_to_blob(self, expression, result_sql)
4509
4510    def round_sql(self, expression: exp.Round) -> str:
4511        this = expression.this
4512        decimals = expression.args.get("decimals")
4513        truncate = expression.args.get("truncate")
4514
4515        # DuckDB requires the scale (decimals) argument to be an INT
4516        # Some dialects (e.g., Snowflake) allow non-integer scales and cast to an integer internally
4517        if decimals is not None and expression.args.get("casts_non_integer_decimals"):
4518            if not (decimals.is_int or decimals.is_type(*exp.DataType.INTEGER_TYPES)):
4519                decimals = exp.cast(decimals, exp.DType.INT)
4520
4521        func = "ROUND"
4522        if truncate:
4523            # BigQuery uses ROUND_HALF_EVEN; Snowflake uses HALF_TO_EVEN
4524            if truncate.this in ("ROUND_HALF_EVEN", "HALF_TO_EVEN"):
4525                func = "ROUND_EVEN"
4526                truncate = None
4527            # BigQuery uses ROUND_HALF_AWAY_FROM_ZERO; Snowflake uses HALF_AWAY_FROM_ZERO
4528            elif truncate.this in ("ROUND_HALF_AWAY_FROM_ZERO", "HALF_AWAY_FROM_ZERO"):
4529                truncate = None
4530
4531        return self.func(func, this, decimals, truncate)
4532
4533    def trycast_sql(self, expression: exp.TryCast) -> str:
4534        to = expression.to
4535        to_type = to.this
4536        src = expression.this
4537
4538        if (
4539            expression.args.get("null_on_text_overflow")
4540            and to_type in exp.DataType.TEXT_TYPES
4541            and to.expressions
4542        ):
4543            return self.sql(
4544                exp.case()
4545                .when(
4546                    exp.LTE(this=exp.func("LENGTH", src), expression=to.expressions[0].this),
4547                    exp.cast(src, "TEXT"),
4548                )
4549                .else_(exp.Null())
4550            )
4551        elif to_type == exp.DType.DATE and expression.args.get("probe_date_format"):
4552            slash_strptime = exp.cast(
4553                exp.func("TRY_STRPTIME", src, exp.Literal.string(self._TRYCAST_DATE_SLASH_FMT)),
4554                "DATE",
4555            )
4556            mon_strptime = exp.cast(
4557                exp.func("TRY_STRPTIME", src, exp.Literal.string(self._TRYCAST_DATE_MON_FMT)),
4558                "DATE",
4559            )
4560            return self.sql(
4561                exp.case()
4562                .when(exp.func("CONTAINS", src, exp.Literal.string("/")), slash_strptime)
4563                .when(
4564                    exp.RegexpLike(this=src, expression=exp.Literal.string("[A-Za-z]")),
4565                    mon_strptime,
4566                )
4567                .else_(exp.TryCast(this=src, to=to))
4568            )
4569        elif (
4570            isinstance(to_type, exp.Interval)
4571            and (unit := to_type.unit)
4572            and expression.args.get("requires_string")
4573        ):
4574            interval_type = exp.DataType.build("INTERVAL")
4575            if isinstance(unit, exp.IntervalSpan):
4576                self.unsupported(
4577                    "TRY_CAST to INTERVAL with span (e.g. HOUR TO MINUTE) is not supported in DuckDB"
4578                )
4579                return self.sql(exp.TryCast(this=src, to=interval_type))
4580            return self.sql(
4581                exp.TryCast(
4582                    this=exp.DPipe(this=src, expression=exp.Literal.string(f" {unit.name}")),
4583                    to=interval_type,
4584                )
4585            )
4586
4587        return super().trycast_sql(expression)
4588
4589    def strtok_sql(self, expression: exp.Strtok) -> str:
4590        string_arg = expression.this
4591        delimiter_arg = expression.args.get("delimiter")
4592        part_index_arg = expression.args.get("part_index")
4593
4594        if delimiter_arg and part_index_arg:
4595            # Escape regex chars and build character class at runtime using REGEXP_REPLACE
4596            escaped_delimiter = exp.Anonymous(
4597                this="REGEXP_REPLACE",
4598                expressions=[
4599                    delimiter_arg,
4600                    exp.Literal.string(
4601                        r"([\[\]^.\-*+?(){}|$\\])"
4602                    ),  # Escape problematic regex chars
4603                    exp.Literal.string(
4604                        r"\\\1"
4605                    ),  # Replace with escaped version using $1 backreference
4606                    exp.Literal.string("g"),  # Global flag
4607                ],
4608            )
4609            # CASE WHEN delimiter = '' THEN '' ELSE CONCAT('[', escaped_delimiter, ']') END
4610            regex_pattern = (
4611                exp.case()
4612                .when(delimiter_arg.eq(exp.Literal.string("")), exp.Literal.string(""))
4613                .else_(
4614                    exp.func(
4615                        "CONCAT",
4616                        exp.Literal.string("["),
4617                        escaped_delimiter,
4618                        exp.Literal.string("]"),
4619                    )
4620                )
4621            )
4622
4623            # STRTOK skips empty strings, so we need to filter them out
4624            # LIST_FILTER(REGEXP_SPLIT_TO_ARRAY(string, pattern), x -> x != '')[index]
4625            split_array = exp.func("REGEXP_SPLIT_TO_ARRAY", string_arg, regex_pattern)
4626            x = exp.to_identifier("x")
4627            is_empty = x.eq(exp.Literal.string(""))
4628            filtered_array = exp.func(
4629                "LIST_FILTER",
4630                split_array,
4631                exp.Lambda(this=exp.not_(is_empty.copy()), expressions=[x.copy()]),
4632            )
4633            base_func = exp.Bracket(
4634                this=filtered_array,
4635                expressions=[part_index_arg],
4636                offset=1,
4637            )
4638
4639            # Use template with the built regex pattern
4640            result = exp.replace_placeholders(
4641                self.STRTOK_TEMPLATE.copy(),
4642                string=string_arg,
4643                delimiter=delimiter_arg,
4644                part_index=part_index_arg,
4645                base_func=base_func,
4646            )
4647
4648            return self.sql(result)
4649
4650        return self.function_fallback_sql(expression)
4651
4652    def strtoktoarray_sql(self, expression: exp.StrtokToArray) -> str:
4653        string_arg = expression.this
4654        delimiter_arg = expression.args.get("expression") or exp.Literal.string(" ")
4655
4656        escaped = exp.RegexpReplace(
4657            this=delimiter_arg.copy(),
4658            expression=exp.Literal.string(r"([\[\]^.\-*+?(){}|$\\])"),
4659            replacement=exp.Literal.string(r"\\\1"),
4660            modifiers=exp.Literal.string("g"),
4661        )
4662        return self.sql(
4663            exp.replace_placeholders(
4664                self.STRTOK_TO_ARRAY_TEMPLATE.copy(),
4665                string=string_arg,
4666                delimiter=delimiter_arg,
4667                escaped=escaped,
4668            )
4669        )
4670
4671    def approxquantile_sql(self, expression: exp.ApproxQuantile) -> str:
4672        result = self.func("APPROX_QUANTILE", expression.this, expression.args.get("quantile"))
4673
4674        # DuckDB returns integers for APPROX_QUANTILE, cast to DOUBLE if the expected type is a real type
4675        if expression.is_type(*exp.DataType.REAL_TYPES):
4676            result = f"CAST({result} AS DOUBLE)"
4677
4678        return result
4679
4680    def approxquantiles_sql(self, expression: exp.ApproxQuantiles) -> str:
4681        """
4682        BigQuery's APPROX_QUANTILES(expr, n) returns an array of n+1 approximate quantile values
4683        dividing the input distribution into n equal-sized buckets.
4684
4685        Both BigQuery and DuckDB use approximate algorithms for quantile estimation, but BigQuery
4686        does not document the specific algorithm used so results may differ. DuckDB does not
4687        support RESPECT NULLS.
4688        """
4689        this = expression.this
4690        if isinstance(this, exp.Distinct):
4691            # APPROX_QUANTILES requires 2 args and DISTINCT node grabs both
4692            if len(this.expressions) < 2:
4693                self.unsupported("APPROX_QUANTILES requires a bucket count argument")
4694                return self.function_fallback_sql(expression)
4695            num_quantiles_expr = this.expressions[1].pop()
4696        else:
4697            num_quantiles_expr = expression.expression
4698
4699        if not isinstance(num_quantiles_expr, exp.Literal) or not num_quantiles_expr.is_int:
4700            self.unsupported("APPROX_QUANTILES bucket count must be a positive integer")
4701            return self.function_fallback_sql(expression)
4702
4703        num_quantiles = t.cast(int, num_quantiles_expr.to_py())
4704        if num_quantiles <= 0:
4705            self.unsupported("APPROX_QUANTILES bucket count must be a positive integer")
4706            return self.function_fallback_sql(expression)
4707
4708        quantiles = [
4709            exp.Literal.number(Decimal(i) / Decimal(num_quantiles))
4710            for i in range(num_quantiles + 1)
4711        ]
4712
4713        return self.sql(exp.ApproxQuantile(this=this, quantile=exp.Array(expressions=quantiles)))
4714
4715    def jsonextractscalar_sql(self, expression: exp.JSONExtractScalar) -> str:
4716        if expression.args.get("scalar_only"):
4717            json_value = exp.JSONExtractScalar(
4718                this=rename_func("JSON_VALUE")(self, expression), expression="'$'"
4719            )
4720
4721            # `->>` binds looser than most operators, so the wrap logic needs the parent
4722            json_value.parent = expression.parent
4723            expression = json_value
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.BinaryColumnConstraint'>: <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.JSONBContainsTopKey'>: <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 = {'using', 'check_p', 'unique', 'into', 'intersect', 'localtimestamp', 'any', 'table', 'order', 'when', 'grant', 'all', 'in_p', 'cast', 'session_user', 'where', 'column', 'symmetric', 'except', 'variadic', 'array', 'current_timestamp', 'true_p', 'deferrable', 'foreign', 'else', 'select', 'some', 'current_date', 'with', 'end_p', 'constraint', 'asymmetric', 'current_user', 'current_catalog', 'current_role', 'not', 'then', 'asc_p', 'trailing', 'default', 'analyse', 'false_p', 'window', 'leading', 'union', 'distinct', 'current_time', 'case', 'and', 'do', 'returning', 'primary', 'localtime', 'on', 'placing', 'group_p', 'lateral_p', 'both', 'null_p', 'only', 'references', 'offset', 'to', 'as', 'initially', 'user', 'having', 'desc_p', 'collate', 'analyze', 'fetch', 'for', 'from', 'create_p', 'or', 'limit'}
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: ClassVar[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: ClassVar[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: ClassVar[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: ClassVar[sqlglot.expressions.core.Expr] = Mod( this=Placeholder(this=base), expression=Placeholder(this=max_val))
SEQ_SIGNED: ClassVar[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: ClassVar[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: ClassVar[dict[str, tuple[str, str]]] = {'WEEKISO': ('%V', 'INTEGER'), 'YEAROFWEEK': ('%G', 'INTEGER'), 'YEAROFWEEKISO': ('%G', 'INTEGER'), 'NANOSECOND': ('%n', 'BIGINT')}
EXTRACT_EPOCH_MAPPINGS: ClassVar[dict[str, str]] = {'EPOCH_SECOND': 'EPOCH', 'EPOCH_MILLISECOND': 'EPOCH_MS', 'EPOCH_MICROSECOND': 'EPOCH_US', 'EPOCH_NANOSECOND': 'EPOCH_NS'}
BITMAP_CONSTRUCT_AGG_TEMPLATE: ClassVar[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: ClassVar[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: ClassVar[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: ClassVar[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: ClassVar[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: ClassVar[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: ClassVar[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: ClassVar[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: ClassVar[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: ClassVar[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: ClassVar[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: ClassVar[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: ClassVar[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: ClassVar[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:
2318    def timeslice_sql(self, expression: exp.TimeSlice) -> str:
2319        """
2320        Transform Snowflake's TIME_SLICE to DuckDB's time_bucket.
2321
2322        Snowflake: TIME_SLICE(date_expr, slice_length, 'UNIT' [, 'START'|'END'])
2323        DuckDB:    time_bucket(INTERVAL 'slice_length' UNIT, date_expr)
2324
2325        For 'END' kind, add the interval to get the end of the slice.
2326        For DATE type with 'END', cast result back to DATE to preserve type.
2327        """
2328        date_expr = expression.this
2329        slice_length = expression.expression
2330        unit = expression.unit
2331        kind = expression.text("kind").upper()
2332
2333        # Create INTERVAL expression: INTERVAL 'N' UNIT
2334        interval_expr = exp.Interval(this=slice_length, unit=unit)
2335
2336        # Create base time_bucket expression
2337        time_bucket_expr = exp.func("time_bucket", interval_expr, date_expr)
2338
2339        # Check if we need the end of the slice (default is start)
2340        if not kind == "END":
2341            # For 'START', return time_bucket directly
2342            return self.sql(time_bucket_expr)
2343
2344        # For 'END', add the interval to get end of slice
2345        add_expr = exp.Add(this=time_bucket_expr, expression=interval_expr.copy())
2346
2347        # If input is DATE type, cast result back to DATE to preserve type
2348        # DuckDB converts DATE to TIMESTAMP when adding intervals
2349        if date_expr.is_type(exp.DType.DATE):
2350            return self.sql(exp.cast(add_expr, exp.DType.DATE))
2351
2352        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:
2354    def bitmapbucketnumber_sql(self, expression: exp.BitmapBucketNumber) -> str:
2355        """
2356        Transpile BITMAP_BUCKET_NUMBER function from Snowflake to DuckDB equivalent.
2357
2358        Snowflake's BITMAP_BUCKET_NUMBER returns a 1-based bucket identifier where:
2359        - Each bucket covers 32,768 values
2360        - Bucket numbering starts at 1
2361        - Formula: ((value - 1) // 32768) + 1 for positive values
2362
2363        For non-positive values (0 and negative), we use value // 32768 to avoid
2364        producing bucket 0 or positive bucket IDs for negative inputs.
2365        """
2366        value = expression.this
2367
2368        positive_formula = ((value - 1) // 32768) + 1
2369        non_positive_formula = value // 32768
2370
2371        # CASE WHEN value > 0 THEN ((value - 1) // 32768) + 1 ELSE value // 32768 END
2372        case_expr = (
2373            exp.case()
2374            .when(exp.GT(this=value, expression=exp.Literal.number(0)), positive_formula)
2375            .else_(non_positive_formula)
2376        )
2377        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:
2379    def bitmapbitposition_sql(self, expression: exp.BitmapBitPosition) -> str:
2380        """
2381        Transpile Snowflake's BITMAP_BIT_POSITION to DuckDB CASE expression.
2382
2383        Snowflake's BITMAP_BIT_POSITION behavior:
2384        - For n <= 0: returns ABS(n) % 32768
2385        - For n > 0: returns (n - 1) % 32768 (maximum return value is 32767)
2386        """
2387        this = expression.this
2388
2389        return self.sql(
2390            exp.Mod(
2391                this=exp.Paren(
2392                    this=exp.If(
2393                        this=exp.GT(this=this, expression=exp.Literal.number(0)),
2394                        true=this - exp.Literal.number(1),
2395                        false=exp.Abs(this=this),
2396                    )
2397                ),
2398                expression=MAX_BIT_POSITION,
2399            )
2400        )

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:
2402    def bitmapconstructagg_sql(self, expression: exp.BitmapConstructAgg) -> str:
2403        """
2404        Transpile Snowflake's BITMAP_CONSTRUCT_AGG to DuckDB equivalent.
2405        Uses a pre-parsed template with placeholders replaced by expression nodes.
2406
2407        Snowflake bitmap format:
2408        - Small (< 5 unique values): 2-byte count (big-endian) + values (little-endian) + padding to 10 bytes
2409        - Large (>= 5 unique values): 10-byte header (0x08 + 9 zeros) + values (little-endian)
2410        """
2411        arg = expression.this
2412        return (
2413            f"({self.sql(exp.replace_placeholders(self.BITMAP_CONSTRUCT_AGG_TEMPLATE, arg=arg))})"
2414        )

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:
2416    def getignorecase_sql(self, expression: exp.GetIgnoreCase) -> str:
2417        self.unsupported("DuckDB does not support the GET_IGNORE_CASE() function")
2418        return self.function_fallback_sql(expression)
def compress_sql(self, expression: sqlglot.expressions.string.Compress) -> str:
2420    def compress_sql(self, expression: exp.Compress) -> str:
2421        self.unsupported("DuckDB does not support the COMPRESS() function")
2422        return self.function_fallback_sql(expression)
def encrypt_sql(self, expression: sqlglot.expressions.string.Encrypt) -> str:
2424    def encrypt_sql(self, expression: exp.Encrypt) -> str:
2425        self.unsupported("ENCRYPT is not supported in DuckDB")
2426        return self.function_fallback_sql(expression)
def decrypt_sql(self, expression: sqlglot.expressions.string.Decrypt) -> str:
2428    def decrypt_sql(self, expression: exp.Decrypt) -> str:
2429        func_name = "TRY_DECRYPT" if expression.args.get("safe") else "DECRYPT"
2430        self.unsupported(f"{func_name} is not supported in DuckDB")
2431        return self.function_fallback_sql(expression)
def decryptraw_sql(self, expression: sqlglot.expressions.string.DecryptRaw) -> str:
2433    def decryptraw_sql(self, expression: exp.DecryptRaw) -> str:
2434        func_name = "TRY_DECRYPT_RAW" if expression.args.get("safe") else "DECRYPT_RAW"
2435        self.unsupported(f"{func_name} is not supported in DuckDB")
2436        return self.function_fallback_sql(expression)
def encryptraw_sql(self, expression: sqlglot.expressions.string.EncryptRaw) -> str:
2438    def encryptraw_sql(self, expression: exp.EncryptRaw) -> str:
2439        self.unsupported("ENCRYPT_RAW is not supported in DuckDB")
2440        return self.function_fallback_sql(expression)
def parseurl_sql(self, expression: sqlglot.expressions.string.ParseUrl) -> str:
2442    def parseurl_sql(self, expression: exp.ParseUrl) -> str:
2443        self.unsupported("PARSE_URL is not supported in DuckDB")
2444        return self.function_fallback_sql(expression)
def parseip_sql(self, expression: sqlglot.expressions.functions.ParseIp) -> str:
2446    def parseip_sql(self, expression: exp.ParseIp) -> str:
2447        self.unsupported("PARSE_IP is not supported in DuckDB")
2448        return self.function_fallback_sql(expression)
def decompressstring_sql(self, expression: sqlglot.expressions.string.DecompressString) -> str:
2450    def decompressstring_sql(self, expression: exp.DecompressString) -> str:
2451        self.unsupported("DECOMPRESS_STRING is not supported in DuckDB")
2452        return self.function_fallback_sql(expression)
def decompressbinary_sql(self, expression: sqlglot.expressions.string.DecompressBinary) -> str:
2454    def decompressbinary_sql(self, expression: exp.DecompressBinary) -> str:
2455        self.unsupported("DECOMPRESS_BINARY is not supported in DuckDB")
2456        return self.function_fallback_sql(expression)
def jarowinklersimilarity_sql(self, expression: sqlglot.expressions.math.JarowinklerSimilarity) -> str:
2458    def jarowinklersimilarity_sql(self, expression: exp.JarowinklerSimilarity) -> str:
2459        this = expression.this
2460        expr = expression.expression
2461
2462        if expression.args.get("case_insensitive"):
2463            this = exp.Upper(this=this)
2464            expr = exp.Upper(this=expr)
2465
2466        result = exp.func("JARO_WINKLER_SIMILARITY", this, expr)
2467
2468        if expression.args.get("integer_scale"):
2469            result = exp.cast(result * 100, "INTEGER")
2470
2471        return self.sql(result)
def randstr_sql(self, expression: sqlglot.expressions.functions.Randstr) -> str:
2473    def randstr_sql(self, expression: exp.Randstr) -> str:
2474        """
2475        Transpile Snowflake's RANDSTR to DuckDB equivalent using deterministic hash-based random.
2476        Uses a pre-parsed template with placeholders replaced by expression nodes.
2477
2478        RANDSTR(length, generator) generates a random string of specified length.
2479        - With numeric seed: Use HASH(i + seed) for deterministic output (same seed = same result)
2480        - With RANDOM(): Use RANDOM() in the hash for non-deterministic output
2481        - No generator: Use default seed value
2482        """
2483        length = expression.this
2484        generator = expression.args.get("generator")
2485
2486        if generator:
2487            if isinstance(generator, exp.Rand):
2488                # If it's RANDOM(), use its seed if available, otherwise use RANDOM() itself
2489                seed_value = generator.this or generator
2490            else:
2491                # Const/int or other expression - use as seed directly
2492                seed_value = generator
2493        else:
2494            # No generator specified, use default seed (arbitrary but deterministic)
2495            seed_value = exp.Literal.number(RANDSTR_SEED)
2496
2497        replacements = {"seed": seed_value, "length": length}
2498        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:
2500    @unsupported_args("finish")
2501    def reduce_sql(self, expression: exp.Reduce) -> str:
2502        array_arg = expression.this
2503        initial_value = expression.args.get("initial")
2504        merge_lambda = expression.args.get("merge")
2505
2506        if merge_lambda:
2507            merge_lambda.set("colon", True)
2508
2509        return self.func("list_reduce", array_arg, merge_lambda, initial_value)
def zipf_sql(self, expression: sqlglot.expressions.functions.Zipf) -> str:
2511    def zipf_sql(self, expression: exp.Zipf) -> str:
2512        """
2513        Transpile Snowflake's ZIPF to DuckDB using CDF-based inverse sampling.
2514        Uses a pre-parsed template with placeholders replaced by expression nodes.
2515        """
2516        s = expression.this
2517        n = expression.args["elementcount"]
2518        gen = expression.args["gen"]
2519
2520        if not isinstance(gen, exp.Rand):
2521            # (ABS(HASH(seed)) % 1000000) / 1000000.0
2522            random_expr: exp.Expr = exp.Div(
2523                this=exp.Paren(
2524                    this=exp.Mod(
2525                        this=exp.Abs(this=exp.Anonymous(this="HASH", expressions=[gen.copy()])),
2526                        expression=exp.Literal.number(1000000),
2527                    )
2528                ),
2529                expression=exp.Literal.number(1000000.0),
2530            )
2531        else:
2532            # Use RANDOM() for non-deterministic output
2533            random_expr = exp.Rand()
2534
2535        replacements = {"s": s, "n": n, "random_expr": random_expr}
2536        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:
2538    def tobinary_sql(self, expression: exp.ToBinary) -> str:
2539        """
2540        TO_BINARY and TRY_TO_BINARY transpilation:
2541        - 'HEX': TO_BINARY('48454C50', 'HEX') -> UNHEX('48454C50')
2542        - 'UTF-8': TO_BINARY('TEST', 'UTF-8') -> ENCODE('TEST')
2543        - 'BASE64': TO_BINARY('SEVMUA==', 'BASE64') -> FROM_BASE64('SEVMUA==')
2544
2545        For TRY_TO_BINARY (safe=True), wrap with TRY():
2546        - 'HEX': TRY_TO_BINARY('invalid', 'HEX') -> TRY(UNHEX('invalid'))
2547        """
2548        value = expression.this
2549        format_arg = expression.args.get("format")
2550        is_safe = expression.args.get("safe")
2551        is_binary = _is_binary(expression)
2552
2553        if not format_arg and not is_binary:
2554            func_name = "TRY_TO_BINARY" if is_safe else "TO_BINARY"
2555            return self.func(func_name, value)
2556
2557        # Snowflake defaults to HEX encoding when no format is specified
2558        fmt = format_arg.name.upper() if format_arg else "HEX"
2559
2560        if fmt in ("UTF-8", "UTF8"):
2561            # DuckDB ENCODE always uses UTF-8, no charset parameter needed
2562            result = self.func("ENCODE", value)
2563        elif fmt == "BASE64":
2564            result = self.func("FROM_BASE64", value)
2565        elif fmt == "HEX":
2566            result = self.func("UNHEX", value)
2567        else:
2568            if is_safe:
2569                return self.sql(exp.null())
2570            else:
2571                self.unsupported(f"format {fmt} is not supported")
2572                result = self.func("TO_BINARY", value)
2573        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:
2575    def tonumber_sql(self, expression: exp.ToNumber) -> str:
2576        fmt = expression.args.get("format")
2577        precision = expression.args.get("precision")
2578        scale = expression.args.get("scale")
2579
2580        if not fmt and precision and scale:
2581            return self.sql(
2582                exp.cast(
2583                    expression.this, f"DECIMAL({precision.name}, {scale.name})", dialect="duckdb"
2584                )
2585            )
2586
2587        return super().tonumber_sql(expression)
def generator_sql(self, expression: sqlglot.expressions.array.Generator) -> str:
2613    def generator_sql(self, expression: exp.Generator) -> str:
2614        # Transpile Snowflake GENERATOR to DuckDB range()
2615        rowcount = expression.args.get("rowcount")
2616        time_limit = expression.args.get("time_limit")
2617
2618        if time_limit:
2619            self.unsupported("GENERATOR TIMELIMIT parameter is not supported in DuckDB")
2620
2621        if not rowcount:
2622            self.unsupported("GENERATOR without ROWCOUNT is not supported in DuckDB")
2623            return self.func("range", exp.Literal.number(0))
2624
2625        return self.func("range", rowcount)
def greatest_sql(self, expression: sqlglot.expressions.functions.Greatest) -> str:
2627    def greatest_sql(self, expression: exp.Greatest) -> str:
2628        return self._greatest_least_sql(expression)
def least_sql(self, expression: sqlglot.expressions.functions.Least) -> str:
2630    def least_sql(self, expression: exp.Least) -> str:
2631        return self._greatest_least_sql(expression)
def lambda_sql( self, expression: sqlglot.expressions.query.Lambda, arrow_sep: str = '->', wrap: bool = True) -> str:
2633    def lambda_sql(self, expression: exp.Lambda, arrow_sep: str = "->", wrap: bool = True) -> str:
2634        if expression.args.get("colon"):
2635            prefix = "LAMBDA "
2636            arrow_sep = ":"
2637            wrap = False
2638        else:
2639            prefix = ""
2640
2641        lambda_sql = super().lambda_sql(expression, arrow_sep=arrow_sep, wrap=wrap)
2642        return f"{prefix}{lambda_sql}"
def show_sql(self, expression: sqlglot.expressions.ddl.Show) -> str:
2644    def show_sql(self, expression: exp.Show) -> str:
2645        from_ = self.sql(expression, "from_")
2646        from_ = f" FROM {from_}" if from_ else ""
2647        return f"SHOW {expression.name}{from_}"
def soundex_sql(self, expression: sqlglot.expressions.string.Soundex) -> str:
2649    def soundex_sql(self, expression: exp.Soundex) -> str:
2650        self.unsupported("SOUNDEX is not supported in DuckDB")
2651        return self.func("SOUNDEX", expression.this)
def sortarray_sql(self, expression: sqlglot.expressions.array.SortArray) -> str:
2653    def sortarray_sql(self, expression: exp.SortArray) -> str:
2654        arr = expression.this
2655        asc = expression.args.get("asc")
2656        nulls_first = expression.args.get("nulls_first")
2657
2658        if not isinstance(asc, exp.Boolean) and not isinstance(nulls_first, exp.Boolean):
2659            return self.func("LIST_SORT", arr, asc, nulls_first)
2660
2661        nulls_are_first = nulls_first == exp.true()
2662        nulls_first_sql = exp.Literal.string("NULLS FIRST") if nulls_are_first else None
2663
2664        if not isinstance(asc, exp.Boolean):
2665            return self.func("LIST_SORT", arr, asc, nulls_first_sql)
2666
2667        descending = asc == exp.false()
2668
2669        if not descending and not nulls_are_first:
2670            return self.func("LIST_SORT", arr)
2671        if not nulls_are_first:
2672            return self.func("ARRAY_REVERSE_SORT", arr)
2673        return self.func(
2674            "LIST_SORT",
2675            arr,
2676            exp.Literal.string("DESC" if descending else "ASC"),
2677            exp.Literal.string("NULLS FIRST"),
2678        )
def install_sql(self, expression: sqlglot.expressions.ddl.Install) -> str:
2680    def install_sql(self, expression: exp.Install) -> str:
2681        force = "FORCE " if expression.args.get("force") else ""
2682        this = self.sql(expression, "this")
2683        from_clause = expression.args.get("from_")
2684        from_clause = f" FROM {from_clause}" if from_clause else ""
2685        return f"{force}INSTALL {this}{from_clause}"
def approxtopk_sql(self, expression: sqlglot.expressions.aggregate.ApproxTopK) -> str:
2687    def approxtopk_sql(self, expression: exp.ApproxTopK) -> str:
2688        self.unsupported(
2689            "APPROX_TOP_K cannot be transpiled to DuckDB due to incompatible return types. "
2690        )
2691        return self.function_fallback_sql(expression)
def strposition_sql(self, expression: sqlglot.expressions.string.StrPosition) -> str:
2693    def strposition_sql(self, expression: exp.StrPosition) -> str:
2694        this = expression.this
2695        substr = expression.args.get("substr")
2696        position = expression.args.get("position")
2697
2698        # For BINARY/BLOB: DuckDB's STRPOS doesn't support BLOB types
2699        # Convert to HEX strings, use STRPOS, then convert hex position to byte position
2700        if _is_binary(this):
2701            # Build expression: STRPOS(HEX(haystack), HEX(needle))
2702            hex_strpos = exp.StrPosition(
2703                this=exp.Hex(this=this),
2704                substr=exp.Hex(this=substr),
2705            )
2706
2707            return self.sql(exp.cast((hex_strpos + 1) / 2, exp.DType.INT))
2708
2709        # For VARCHAR: handle clamp_position
2710        if expression.args.get("clamp_position") and position:
2711            expression = expression.copy()
2712            expression.set(
2713                "position",
2714                exp.If(
2715                    this=exp.LTE(this=position, expression=exp.Literal.number(0)),
2716                    true=exp.Literal.number(1),
2717                    false=position.copy(),
2718                ),
2719            )
2720
2721        return strposition_sql(self, expression)
def substring_sql(self, expression: sqlglot.expressions.string.Substring) -> str:
2723    def substring_sql(self, expression: exp.Substring) -> str:
2724        if expression.args.get("zero_start"):
2725            start = expression.args.get("start")
2726            length = expression.args.get("length")
2727
2728            if start := expression.args.get("start"):
2729                start = exp.If(this=start.eq(0), true=exp.Literal.number(1), false=start)
2730            if length := expression.args.get("length"):
2731                length = exp.If(this=length < 0, true=exp.Literal.number(0), false=length)
2732
2733            return self.func("SUBSTRING", expression.this, start, length)
2734
2735        return self.function_fallback_sql(expression)
def strtotime_sql(self, expression: sqlglot.expressions.temporal.StrToTime) -> str:
2737    def strtotime_sql(self, expression: exp.StrToTime) -> str:
2738        # Check if target_type requires TIMESTAMPTZ (for LTZ/TZ variants)
2739        target_type = expression.args.get("target_type")
2740        needs_tz = target_type and target_type.this in (
2741            exp.DType.TIMESTAMPLTZ,
2742            exp.DType.TIMESTAMPTZ,
2743        )
2744
2745        value, formatted_time = self._strptime_default_year(expression)
2746
2747        if expression.args.get("safe"):
2748            cast_type = exp.DType.TIMESTAMPTZ if needs_tz else exp.DType.TIMESTAMP
2749            return self.sql(exp.cast(self.func("TRY_STRPTIME", value, formatted_time), cast_type))
2750
2751        base_sql = self.func("STRPTIME", value, formatted_time)
2752        if needs_tz:
2753            return self.sql(
2754                exp.cast(
2755                    base_sql,
2756                    exp.DataType(this=exp.DType.TIMESTAMPTZ),
2757                )
2758            )
2759        return base_sql
def strtodate_sql(self, expression: sqlglot.expressions.temporal.StrToDate) -> str:
2761    def strtodate_sql(self, expression: exp.StrToDate) -> str:
2762        value, formatted_time = self._strptime_default_year(expression)
2763        function_name = "STRPTIME" if not expression.args.get("safe") else "TRY_STRPTIME"
2764        return self.sql(
2765            exp.cast(
2766                self.func(function_name, value, formatted_time),
2767                exp.DataType(this=exp.DType.DATE),
2768            )
2769        )
def parsedatetime_sql(self, expression: sqlglot.expressions.temporal.ParseDatetime) -> str:
2783    def parsedatetime_sql(self, expression: exp.ParseDatetime) -> str:
2784        value, formatted_time = self._strptime_default_year(expression)
2785        return self.func("STRPTIME", value, formatted_time)
def parsetime_sql(self, expression: sqlglot.expressions.temporal.ParseTime) -> str:
2787    def parsetime_sql(self, expression: exp.ParseTime) -> str:
2788        formatted_time = self.format_time(expression)
2789        return self.sql(
2790            exp.cast(
2791                self.func("STRPTIME", expression.this, formatted_time),
2792                exp.DataType(this=exp.DType.TIME),
2793            )
2794        )
def tsordstotime_sql(self, expression: sqlglot.expressions.temporal.TsOrDsToTime) -> str:
2796    def tsordstotime_sql(self, expression: exp.TsOrDsToTime) -> str:
2797        this = expression.this
2798        time_format = self.format_time(expression)
2799        safe = expression.args.get("safe")
2800        time_type = exp.DataType.from_str("TIME", dialect="duckdb")
2801        cast_expr = exp.TryCast if safe else exp.Cast
2802
2803        if time_format:
2804            func_name = "TRY_STRPTIME" if safe else "STRPTIME"
2805            strptime = exp.Anonymous(this=func_name, expressions=[this, time_format])
2806            return self.sql(cast_expr(this=strptime, to=time_type))
2807
2808        if isinstance(this, exp.TsOrDsToTime) or this.is_type(exp.DType.TIME):
2809            return self.sql(this)
2810
2811        return self.sql(cast_expr(this=this, to=time_type))
def currentdate_sql(self, expression: sqlglot.expressions.temporal.CurrentDate) -> str:
2813    def currentdate_sql(self, expression: exp.CurrentDate) -> str:
2814        if not expression.this:
2815            return "CURRENT_DATE"
2816
2817        expr = exp.Cast(
2818            this=exp.AtTimeZone(this=exp.CurrentTimestamp(), zone=expression.this),
2819            to=exp.DataType(this=exp.DType.DATE),
2820        )
2821        return self.sql(expr)
def checkjson_sql(self, expression: sqlglot.expressions.json.CheckJson) -> str:
2823    def checkjson_sql(self, expression: exp.CheckJson) -> str:
2824        arg = expression.this
2825        return self.sql(
2826            exp.case()
2827            .when(
2828                exp.or_(arg.is_(exp.Null()), arg.eq(""), exp.func("json_valid", arg)),
2829                exp.null(),
2830            )
2831            .else_(exp.Literal.string("Invalid JSON"))
2832        )
def parsejson_sql(self, expression: sqlglot.expressions.json.ParseJSON) -> str:
2834    def parsejson_sql(self, expression: exp.ParseJSON) -> str:
2835        arg = expression.this
2836        if expression.args.get("safe"):
2837            return self.sql(
2838                exp.case()
2839                .when(exp.func("json_valid", arg), exp.cast(arg.copy(), "JSON"))
2840                .else_(exp.null())
2841            )
2842        return self.func("JSON", arg)
def unicode_sql(self, expression: sqlglot.expressions.string.Unicode) -> str:
2844    def unicode_sql(self, expression: exp.Unicode) -> str:
2845        if expression.args.get("empty_is_zero"):
2846            return self.sql(
2847                exp.case()
2848                .when(expression.this.eq(exp.Literal.string("")), exp.Literal.number(0))
2849                .else_(exp.Anonymous(this="UNICODE", expressions=[expression.this]))
2850            )
2851
2852        return self.func("UNICODE", expression.this)
def stripnullvalue_sql(self, expression: sqlglot.expressions.json.StripNullValue) -> str:
2854    def stripnullvalue_sql(self, expression: exp.StripNullValue) -> str:
2855        return self.sql(
2856            exp.case()
2857            .when(exp.func("json_type", expression.this).eq("NULL"), exp.null())
2858            .else_(expression.this)
2859        )
def trunc_sql(self, expression: sqlglot.expressions.math.Trunc) -> str:
2861    def trunc_sql(self, expression: exp.Trunc) -> str:
2862        decimals = expression.args.get("decimals")
2863        if (
2864            expression.args.get("fractions_supported")
2865            and decimals
2866            and not decimals.is_type(exp.DType.INT)
2867        ):
2868            decimals = exp.cast(decimals, exp.DType.INT, dialect="duckdb")
2869
2870        return self.func("TRUNC", expression.this, decimals)
def normal_sql(self, expression: sqlglot.expressions.functions.Normal) -> str:
2872    def normal_sql(self, expression: exp.Normal) -> str:
2873        """
2874        Transpile Snowflake's NORMAL(mean, stddev, gen) to DuckDB.
2875
2876        Uses the Box-Muller transform via NORMAL_TEMPLATE.
2877        """
2878        mean = expression.this
2879        stddev = expression.args["stddev"]
2880        gen: exp.Expr = expression.args["gen"]
2881
2882        # Build two uniform random values [0, 1) for Box-Muller transform
2883        if isinstance(gen, exp.Rand) and gen.this is None:
2884            u1: exp.Expr = exp.Rand()
2885            u2: exp.Expr = exp.Rand()
2886        else:
2887            # Seeded: derive two values using HASH with different inputs
2888            seed = gen.this if isinstance(gen, exp.Rand) else gen
2889            u1 = exp.replace_placeholders(self.SEEDED_RANDOM_TEMPLATE, seed=seed)
2890            u2 = exp.replace_placeholders(
2891                self.SEEDED_RANDOM_TEMPLATE,
2892                seed=exp.Add(this=seed.copy(), expression=exp.Literal.number(1)),
2893            )
2894
2895        replacements = {"mean": mean, "stddev": stddev, "u1": u1, "u2": u2}
2896        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:
2898    def uniform_sql(self, expression: exp.Uniform) -> str:
2899        """
2900        Transpile Snowflake's UNIFORM(min, max, gen) to DuckDB.
2901
2902        UNIFORM returns a random value in [min, max]:
2903        - Integer result if both min and max are integers
2904        - Float result if either min or max is a float
2905        """
2906        min_val = expression.this
2907        max_val = expression.expression
2908        gen = expression.args.get("gen")
2909
2910        # Determine if result should be integer (both bounds are integers).
2911        # We do this to emulate Snowflake's behavior, INT -> INT, FLOAT -> FLOAT
2912        is_int_result = min_val.is_int and max_val.is_int
2913
2914        # Build the random value expression [0, 1)
2915        if not isinstance(gen, exp.Rand):
2916            # Seed value: (ABS(HASH(seed)) % 1000000) / 1000000.0
2917            random_expr: exp.Expr = exp.Div(
2918                this=exp.Paren(
2919                    this=exp.Mod(
2920                        this=exp.Abs(this=exp.Anonymous(this="HASH", expressions=[gen])),
2921                        expression=exp.Literal.number(1000000),
2922                    )
2923                ),
2924                expression=exp.Literal.number(1000000.0),
2925            )
2926        else:
2927            random_expr = exp.Rand()
2928
2929        # Build: min + random * (max - min [+ 1 for int])
2930        range_expr: exp.Expr = exp.Sub(this=max_val, expression=min_val)
2931        if is_int_result:
2932            range_expr = exp.Add(this=range_expr, expression=exp.Literal.number(1))
2933
2934        result: exp.Expr = exp.Add(
2935            this=min_val,
2936            expression=exp.Mul(this=random_expr, expression=exp.Paren(this=range_expr)),
2937        )
2938
2939        if is_int_result:
2940            result = exp.Cast(this=exp.Floor(this=result), to=exp.DType.BIGINT.into_expr())
2941
2942        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:
2944    def timefromparts_sql(self, expression: exp.TimeFromParts) -> str:
2945        nano = expression.args.get("nano")
2946        overflow = expression.args.get("overflow")
2947
2948        # Snowflake's TIME_FROM_PARTS supports overflow
2949        if overflow:
2950            hour = expression.args["hour"]
2951            minute = expression.args["min"]
2952            sec = expression.args["sec"]
2953
2954            # Check if values are within normal ranges - use MAKE_TIME for efficiency
2955            if not nano and all(arg.is_int for arg in [hour, minute, sec]):
2956                try:
2957                    h_val = hour.to_py()
2958                    m_val = minute.to_py()
2959                    s_val = sec.to_py()
2960                    if 0 <= h_val <= 23 and 0 <= m_val <= 59 and 0 <= s_val <= 59:
2961                        return rename_func("MAKE_TIME")(self, expression)
2962                except ValueError:
2963                    pass
2964
2965            # Overflow or nanoseconds detected - use INTERVAL arithmetic
2966            if nano:
2967                sec = sec + nano.pop() / exp.Literal.number(1000000000.0)
2968
2969            total_seconds = hour * exp.Literal.number(3600) + minute * exp.Literal.number(60) + sec
2970
2971            return self.sql(
2972                exp.Add(
2973                    this=exp.Cast(
2974                        this=exp.Literal.string("00:00:00"), to=exp.DType.TIME.into_expr()
2975                    ),
2976                    expression=exp.Interval(this=total_seconds, unit=exp.var("SECOND")),
2977                )
2978            )
2979
2980        # Default: MAKE_TIME
2981        if nano:
2982            expression.set(
2983                "sec", expression.args["sec"] + nano.pop() / exp.Literal.number(1000000000.0)
2984            )
2985
2986        return rename_func("MAKE_TIME")(self, expression)
def extract_sql(self, expression: sqlglot.expressions.temporal.Extract) -> str:
2988    def extract_sql(self, expression: exp.Extract) -> str:
2989        """
2990        Transpile EXTRACT/DATE_PART for DuckDB, handling specifiers not natively supported.
2991
2992        DuckDB doesn't support: WEEKISO, YEAROFWEEK, YEAROFWEEKISO, NANOSECOND,
2993        EPOCH_SECOND (as integer), EPOCH_MILLISECOND, EPOCH_MICROSECOND, EPOCH_NANOSECOND
2994        """
2995        this = expression.this
2996        datetime_expr = expression.expression
2997
2998        # TIMESTAMPTZ extractions may produce different results between Snowflake and DuckDB
2999        # because Snowflake applies server timezone while DuckDB uses local timezone
3000        if datetime_expr.is_type(exp.DType.TIMESTAMPTZ, exp.DType.TIMESTAMPLTZ):
3001            self.unsupported(
3002                "EXTRACT from TIMESTAMPTZ / TIMESTAMPLTZ may produce different results due to timezone handling differences"
3003            )
3004
3005        part_name = this.name.upper()
3006
3007        if part_name in self.EXTRACT_STRFTIME_MAPPINGS:
3008            fmt, cast_type = self.EXTRACT_STRFTIME_MAPPINGS[part_name]
3009
3010            # Problem: strftime doesn't accept TIME and there's no NANOSECOND function
3011            # So, for NANOSECOND with TIME, fallback to MICROSECOND * 1000
3012            is_nano_time = part_name == "NANOSECOND" and datetime_expr.is_type(
3013                exp.DType.TIME, exp.DType.TIMETZ
3014            )
3015
3016            if is_nano_time:
3017                self.unsupported("Parameter NANOSECOND is not supported with TIME type in DuckDB")
3018                return self.sql(
3019                    exp.cast(
3020                        exp.Mul(
3021                            this=exp.Extract(this=exp.var("MICROSECOND"), expression=datetime_expr),
3022                            expression=exp.Literal.number(1000),
3023                        ),
3024                        exp.DataType.from_str(cast_type, dialect="duckdb"),
3025                    )
3026                )
3027
3028            # For NANOSECOND, cast to TIMESTAMP_NS to preserve nanosecond precision
3029            strftime_input = datetime_expr
3030            if part_name == "NANOSECOND":
3031                strftime_input = exp.cast(datetime_expr, exp.DType.TIMESTAMP_NS)
3032
3033            return self.sql(
3034                exp.cast(
3035                    exp.Anonymous(
3036                        this="STRFTIME",
3037                        expressions=[strftime_input, exp.Literal.string(fmt)],
3038                    ),
3039                    exp.DataType.from_str(cast_type, dialect="duckdb"),
3040                )
3041            )
3042
3043        if part_name in self.EXTRACT_EPOCH_MAPPINGS:
3044            func_name = self.EXTRACT_EPOCH_MAPPINGS[part_name]
3045            result: exp.Expr = exp.Anonymous(this=func_name, expressions=[datetime_expr])
3046            # EPOCH returns float, cast to BIGINT for integer result
3047            if part_name == "EPOCH_SECOND":
3048                result = exp.cast(result, exp.DataType.from_str("BIGINT", dialect="duckdb"))
3049            return self.sql(result)
3050
3051        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:
3053    def timestampfromparts_sql(self, expression: exp.TimestampFromParts) -> str:
3054        # Check if this is the date/time expression form: TIMESTAMP_FROM_PARTS(date_expr, time_expr)
3055        date_expr = expression.this
3056        time_expr = expression.expression
3057
3058        if date_expr is not None and time_expr is not None:
3059            # In DuckDB, DATE + TIME produces TIMESTAMP
3060            return self.sql(exp.Add(this=date_expr, expression=time_expr))
3061
3062        # Component-based form: TIMESTAMP_FROM_PARTS(year, month, day, hour, minute, second, ...)
3063        sec = expression.args.get("sec")
3064        if sec is None:
3065            # This shouldn't happen with valid input, but handle gracefully
3066            return rename_func("MAKE_TIMESTAMP")(self, expression)
3067
3068        milli = expression.args.get("milli")
3069        if milli is not None:
3070            sec += milli.pop() / exp.Literal.number(1000.0)
3071
3072        nano = expression.args.get("nano")
3073        if nano is not None:
3074            sec += nano.pop() / exp.Literal.number(1000000000.0)
3075
3076        if milli or nano:
3077            expression.set("sec", sec)
3078
3079        return rename_func("MAKE_TIMESTAMP")(self, expression)
@unsupported_args('nano')
def timestampltzfromparts_sql( self, expression: sqlglot.expressions.temporal.TimestampLtzFromParts) -> str:
3081    @unsupported_args("nano")
3082    def timestampltzfromparts_sql(self, expression: exp.TimestampLtzFromParts) -> str:
3083        # Pop nano so rename_func only passes args that MAKE_TIMESTAMP accepts
3084        if nano := expression.args.get("nano"):
3085            nano.pop()
3086
3087        timestamp = rename_func("MAKE_TIMESTAMP")(self, expression)
3088        return f"CAST({timestamp} AS TIMESTAMPTZ)"
@unsupported_args('nano')
def timestamptzfromparts_sql( self, expression: sqlglot.expressions.temporal.TimestampTzFromParts) -> str:
3090    @unsupported_args("nano")
3091    def timestamptzfromparts_sql(self, expression: exp.TimestampTzFromParts) -> str:
3092        # Extract zone before popping
3093        zone = expression.args.get("zone")
3094        # Pop zone and nano so rename_func only passes args that MAKE_TIMESTAMP accepts
3095        if zone:
3096            zone = zone.pop()
3097
3098        if nano := expression.args.get("nano"):
3099            nano.pop()
3100
3101        timestamp = rename_func("MAKE_TIMESTAMP")(self, expression)
3102
3103        if zone:
3104            # Use AT TIME ZONE to apply the explicit timezone
3105            return f"{timestamp} AT TIME ZONE {self.sql(zone)}"
3106
3107        return timestamp
def tablesample_sql( self, expression: sqlglot.expressions.query.TableSample, tablesample_keyword: str | None = None) -> str:
3109    def tablesample_sql(
3110        self,
3111        expression: exp.TableSample,
3112        tablesample_keyword: str | None = None,
3113    ) -> str:
3114        if not isinstance(expression.parent, exp.Select):
3115            # This sample clause only applies to a single source, not the entire resulting relation
3116            tablesample_keyword = "TABLESAMPLE"
3117
3118        if expression.args.get("size"):
3119            method = expression.args.get("method")
3120            if method and method.name.upper() != "RESERVOIR":
3121                self.unsupported(
3122                    f"Sampling method {method} is not supported with a discrete sample count, "
3123                    "defaulting to reservoir sampling"
3124                )
3125                expression.set("method", exp.var("RESERVOIR"))
3126
3127        return super().tablesample_sql(expression, tablesample_keyword=tablesample_keyword)
def in_sql(self, expression: sqlglot.expressions.core.In) -> str:
3129    def in_sql(self, expression: exp.In) -> str:
3130        unnest = expression.args.get("unnest")
3131        if unnest:
3132            return self.sql(
3133                exp.replace_placeholders(
3134                    self.IN_UNNEST_TEMPLATE, arr=unnest.expressions[0], value=expression.this
3135                )
3136            )
3137        return super().in_sql(expression)
def join_sql(self, expression: sqlglot.expressions.query.Join) -> str:
3139    def join_sql(self, expression: exp.Join) -> str:
3140        if (
3141            not expression.args.get("using")
3142            and not expression.args.get("on")
3143            and not expression.method
3144            and (expression.kind in ("", "INNER", "OUTER"))
3145        ):
3146            # Some dialects support `LEFT/INNER JOIN UNNEST(...)` without an explicit ON clause
3147            # DuckDB doesn't, but we can just add a dummy ON clause that is always true
3148            if isinstance(expression.this, exp.Unnest):
3149                return super().join_sql(expression.on(exp.true()))
3150
3151            expression.set("side", None)
3152            expression.set("kind", None)
3153
3154        return super().join_sql(expression)
def countif_sql(self, expression: sqlglot.expressions.aggregate.CountIf) -> str:
3156    def countif_sql(self, expression: exp.CountIf) -> str:
3157        if self.dialect.version >= (1, 2):
3158            this = expression.this
3159            if expression.args.get("zero_on_all_null") and not isinstance(this, exp.Distinct):
3160                # DuckDB >= 1.2's COUNT_IF returns NULL when the condition is NULL on all rows,
3161                # so we wrap the condition in IS TRUE to preserve count-like semantics
3162                expression = exp.CountIf(this=exp.paren(this).is_(exp.true()))
3163            return self.function_fallback_sql(expression)
3164
3165        # https://github.com/tobymao/sqlglot/pull/4749
3166        return count_if_to_sum(self, expression)
def bracket_sql(self, expression: sqlglot.expressions.core.Bracket) -> str:
3168    def bracket_sql(self, expression: exp.Bracket) -> str:
3169        if self.dialect.version >= (1, 2):
3170            return super().bracket_sql(expression)
3171
3172        # https://duckdb.org/2025/02/05/announcing-duckdb-120.html#breaking-changes
3173        this = expression.this
3174        if isinstance(this, exp.Array):
3175            this.replace(exp.paren(this))
3176
3177        bracket = super().bracket_sql(expression)
3178
3179        if not expression.args.get("returns_list_for_maps"):
3180            if not this.type:
3181                from sqlglot.optimizer.annotate_types import annotate_types
3182
3183                this = annotate_types(this, dialect=self.dialect)
3184
3185            if this.is_type(exp.DType.MAP):
3186                bracket = f"({bracket})[1]"
3187
3188        return bracket
def withingroup_sql(self, expression: sqlglot.expressions.core.WithinGroup) -> str:
3190    def withingroup_sql(self, expression: exp.WithinGroup) -> str:
3191        func = expression.this
3192
3193        # For ARRAY_AGG, DuckDB requires ORDER BY inside the function, not in WITHIN GROUP
3194        # Transform: ARRAY_AGG(x) WITHIN GROUP (ORDER BY y) -> ARRAY_AGG(x ORDER BY y)
3195        if isinstance(func, exp.ArrayAgg):
3196            if not isinstance(order := expression.expression, exp.Order):
3197                return self.sql(func)
3198
3199            # Save the original column for FILTER clause (before wrapping with Order)
3200            original_this = func.this
3201
3202            # Move ORDER BY inside ARRAY_AGG by wrapping its argument with Order
3203            # ArrayAgg.this should become Order(this=ArrayAgg.this, expressions=order.expressions)
3204            func.set(
3205                "this",
3206                exp.Order(
3207                    this=func.this.copy(),
3208                    expressions=order.expressions,
3209                ),
3210            )
3211
3212            # Generate the ARRAY_AGG function with ORDER BY and add FILTER clause if needed
3213            # Use original_this (not the Order-wrapped version) for the FILTER condition
3214            array_agg_sql = self.function_fallback_sql(func)
3215            return self._add_arrayagg_null_filter(array_agg_sql, func, original_this)
3216
3217        # For other functions (like PERCENTILES), use existing logic
3218        expression_sql = self.sql(expression, "expression")
3219
3220        if isinstance(func, exp.PERCENTILES):
3221            # Make the order key the first arg and slide the fraction to the right
3222            # https://duckdb.org/docs/sql/aggregates#ordered-set-aggregate-functions
3223            order_col = expression.find(exp.Ordered)
3224            if order_col:
3225                func.set("expression", func.this)
3226                func.set("this", order_col.this)
3227
3228        this = self.sql(expression, "this").rstrip(")")
3229
3230        return f"{this}{expression_sql})"
def length_sql(self, expression: sqlglot.expressions.string.Length) -> str:
3232    def length_sql(self, expression: exp.Length) -> str:
3233        arg = expression.this
3234
3235        # Dialects like BQ and Snowflake also accept binary values as args, so
3236        # DDB will attempt to infer the type or resort to case/when resolution
3237        if not expression.args.get("binary") or arg.is_string:
3238            return self.func("LENGTH", arg)
3239
3240        if not arg.type:
3241            from sqlglot.optimizer.annotate_types import annotate_types
3242
3243            arg = annotate_types(arg, dialect=self.dialect)
3244
3245        if arg.is_type(*exp.DataType.TEXT_TYPES):
3246            return self.func("LENGTH", arg)
3247
3248        # We need these casts to make duckdb's static type checker happy
3249        blob = exp.cast(arg, exp.DType.VARBINARY)
3250        varchar = exp.cast(arg, exp.DType.VARCHAR)
3251
3252        case = (
3253            exp.case(exp.Anonymous(this="TYPEOF", expressions=[arg]))
3254            .when(exp.Literal.string("BLOB"), exp.ByteLength(this=blob))
3255            .else_(exp.Anonymous(this="LENGTH", expressions=[varchar]))
3256        )
3257        return self.sql(case)
def bitlength_sql(self, expression: sqlglot.expressions.string.BitLength) -> str:
3259    def bitlength_sql(self, expression: exp.BitLength) -> str:
3260        if not _is_binary(arg := expression.this):
3261            return self.func("BIT_LENGTH", arg)
3262
3263        blob = exp.cast(arg, exp.DataType.Type.VARBINARY)
3264        return self.sql(exp.ByteLength(this=blob) * exp.Literal.number(8))
def chr_sql( self, expression: sqlglot.expressions.string.Chr, name: str = 'CHR') -> str:
3266    def chr_sql(self, expression: exp.Chr, name: str = "CHR") -> str:
3267        arg = expression.expressions[0]
3268        if arg.is_type(*exp.DataType.REAL_TYPES):
3269            arg = exp.cast(arg, exp.DType.INT)
3270        return self.func("CHR", arg)
def collation_sql(self, expression: sqlglot.expressions.functions.Collation) -> str:
3272    def collation_sql(self, expression: exp.Collation) -> str:
3273        self.unsupported("COLLATION function is not supported by DuckDB")
3274        return self.function_fallback_sql(expression)
def collate_sql(self, expression: sqlglot.expressions.functions.Collate) -> str:
3276    def collate_sql(self, expression: exp.Collate) -> str:
3277        if not expression.expression.is_string:
3278            return super().collate_sql(expression)
3279
3280        raw = expression.expression.name
3281        if not raw:
3282            return self.sql(expression.this)
3283
3284        parts = []
3285        for part in raw.split("-"):
3286            lower = part.lower()
3287            if lower not in _SNOWFLAKE_COLLATION_DEFAULTS:
3288                if lower in _SNOWFLAKE_COLLATION_UNSUPPORTED:
3289                    self.unsupported(
3290                        f"Snowflake collation specifier '{part}' has no DuckDB equivalent"
3291                    )
3292                parts.append(lower)
3293
3294        if not parts:
3295            return self.sql(expression.this)
3296        return super().collate_sql(
3297            exp.Collate(this=expression.this, expression=exp.var(".".join(parts)))
3298        )
def regexpcount_sql(self, expression: sqlglot.expressions.string.RegexpCount) -> str:
3330    def regexpcount_sql(self, expression: exp.RegexpCount) -> str:
3331        this = expression.this
3332        pattern = expression.expression
3333        position = expression.args.get("position")
3334        parameters = expression.args.get("parameters")
3335
3336        # Validate flags - only "ims" flags are supported for embedded patterns
3337        validated_flags = self._validate_regexp_flags(parameters, supported_flags="ims")
3338
3339        if position:
3340            this = exp.Substring(this=this, start=position)
3341
3342        # Embed flags in pattern (REGEXP_EXTRACT_ALL doesn't support flags argument)
3343        if validated_flags:
3344            pattern = exp.Concat(expressions=[exp.Literal.string(f"(?{validated_flags})"), pattern])
3345
3346        # Handle empty pattern: Snowflake returns 0, DuckDB would match between every character
3347        result = (
3348            exp.case()
3349            .when(
3350                exp.EQ(this=pattern, expression=exp.Literal.string("")),
3351                exp.Literal.number(0),
3352            )
3353            .else_(
3354                exp.Length(
3355                    this=exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern])
3356                )
3357            )
3358        )
3359
3360        return self.sql(result)
def regexpreplace_sql(self, expression: sqlglot.expressions.string.RegexpReplace) -> str:
3362    def regexpreplace_sql(self, expression: exp.RegexpReplace) -> str:
3363        subject = expression.this
3364        pattern = expression.expression
3365        replacement = expression.args.get("replacement") or exp.Literal.string("")
3366        position = expression.args.get("position")
3367        occurrence = expression.args.get("occurrence")
3368        modifiers = expression.args.get("modifiers")
3369
3370        validated_flags = self._validate_regexp_flags(modifiers, supported_flags="cimsg") or ""
3371
3372        # Handle occurrence (only literals supported)
3373        if occurrence and not occurrence.is_int:
3374            self.unsupported("REGEXP_REPLACE with non-literal occurrence")
3375        else:
3376            occurrence = occurrence.to_py() if occurrence and occurrence.is_int else 0
3377            if occurrence > 1:
3378                self.unsupported(f"REGEXP_REPLACE occurrence={occurrence} not supported")
3379            # flag duckdb to do either all or none, single_replace check is for duckdb round trip
3380            elif (
3381                occurrence == 0
3382                and "g" not in validated_flags
3383                and not expression.args.get("single_replace")
3384            ):
3385                validated_flags += "g"
3386
3387        # Handle position (only literals supported)
3388        prefix = None
3389        if position and not position.is_int:
3390            self.unsupported("REGEXP_REPLACE with non-literal position")
3391        elif position and position.is_int and position.to_py() > 1:
3392            pos = position.to_py()
3393            prefix = exp.Substring(
3394                this=subject, start=exp.Literal.number(1), length=exp.Literal.number(pos - 1)
3395            )
3396            subject = exp.Substring(this=subject, start=exp.Literal.number(pos))
3397
3398        result: exp.Expr = exp.Anonymous(
3399            this="REGEXP_REPLACE",
3400            expressions=[
3401                subject,
3402                pattern,
3403                replacement,
3404                exp.Literal.string(validated_flags) if validated_flags else None,
3405            ],
3406        )
3407
3408        if prefix:
3409            result = exp.Concat(expressions=[prefix, result])
3410
3411        return self.sql(result)
def regexplike_sql(self, expression: sqlglot.expressions.core.RegexpLike) -> str:
3413    def regexplike_sql(self, expression: exp.RegexpLike) -> str:
3414        this = expression.this
3415        pattern = expression.expression
3416        flag = expression.args.get("flag")
3417
3418        if expression.args.get("full_match"):
3419            validated_flags = self._validate_regexp_flags(flag, supported_flags="cims")
3420            flag = exp.Literal.string(validated_flags) if validated_flags else None
3421            return self.func("REGEXP_FULL_MATCH", this, pattern, flag)
3422
3423        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:
3425    @unsupported_args("ins_cost", "del_cost", "sub_cost")
3426    def levenshtein_sql(self, expression: exp.Levenshtein) -> str:
3427        this = expression.this
3428        expr = expression.expression
3429        max_dist = expression.args.get("max_dist")
3430
3431        if max_dist is None:
3432            return self.func("LEVENSHTEIN", this, expr)
3433
3434        # Emulate Snowflake semantics: if distance > max_dist, return max_dist
3435        levenshtein = exp.Levenshtein(this=this, expression=expr)
3436        return self.sql(exp.Least(this=levenshtein, expressions=[max_dist]))
def pad_sql(self, expression: sqlglot.expressions.string.Pad) -> str:
3438    def pad_sql(self, expression: exp.Pad) -> str:
3439        """
3440        Handle RPAD/LPAD for VARCHAR and BINARY types.
3441
3442        For VARCHAR: Delegate to parent class
3443        For BINARY: Lower to: input || REPEAT(pad, GREATEST(0, target_len - OCTET_LENGTH(input)))
3444        """
3445        string_arg = expression.this
3446        fill_arg = expression.args.get("fill_pattern") or exp.Literal.string(" ")
3447
3448        if _is_binary(string_arg) or _is_binary(fill_arg):
3449            length_arg = expression.expression
3450            is_left = expression.args.get("is_left")
3451
3452            input_len = exp.ByteLength(this=string_arg)
3453            chars_needed = length_arg - input_len
3454            pad_count = exp.Greatest(
3455                this=exp.Literal.number(0), expressions=[chars_needed], ignore_nulls=True
3456            )
3457            repeat_expr = exp.Repeat(this=fill_arg, times=pad_count)
3458
3459            left, right = string_arg, repeat_expr
3460            if is_left:
3461                left, right = right, left
3462
3463            result = exp.DPipe(this=left, expression=right)
3464            return self.sql(result)
3465
3466        # For VARCHAR: Delegate to parent class (handles PAD_FILL_PATTERN_IS_REQUIRED)
3467        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:
3469    def minhash_sql(self, expression: exp.Minhash) -> str:
3470        k = expression.this
3471        exprs = expression.expressions
3472
3473        if len(exprs) != 1 or isinstance(exprs[0], exp.Star):
3474            self.unsupported(
3475                "MINHASH with multiple expressions or * requires manual query restructuring"
3476            )
3477            return self.func("MINHASH", k, *exprs)
3478
3479        expr = exprs[0]
3480        result = exp.replace_placeholders(self.MINHASH_TEMPLATE.copy(), expr=expr, k=k)
3481        return f"({self.sql(result)})"
def minhashcombine_sql(self, expression: sqlglot.expressions.aggregate.MinhashCombine) -> str:
3483    def minhashcombine_sql(self, expression: exp.MinhashCombine) -> str:
3484        expr = expression.this
3485        result = exp.replace_placeholders(self.MINHASH_COMBINE_TEMPLATE.copy(), expr=expr)
3486        return f"({self.sql(result)})"
def approximatesimilarity_sql( self, expression: sqlglot.expressions.aggregate.ApproximateSimilarity) -> str:
3488    def approximatesimilarity_sql(self, expression: exp.ApproximateSimilarity) -> str:
3489        expr = expression.this
3490        result = exp.replace_placeholders(self.APPROXIMATE_SIMILARITY_TEMPLATE.copy(), expr=expr)
3491        return f"({self.sql(result)})"
def arrayuniqueagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayUniqueAgg) -> str:
3493    def arrayuniqueagg_sql(self, expression: exp.ArrayUniqueAgg) -> str:
3494        return self.sql(
3495            exp.Filter(
3496                this=exp.func("LIST", exp.Distinct(expressions=[expression.this])),
3497                expression=exp.Where(this=expression.this.copy().is_(exp.null()).not_()),
3498            )
3499        )
def arrayconcatagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayConcatAgg) -> str:
3501    def arrayconcatagg_sql(self, expression: exp.ArrayConcatAgg) -> str:
3502        this = expression.this
3503
3504        if isinstance(this, exp.Limit):
3505            self.unsupported("LIMIT in ARRAY_CONCAT_AGG cannot be transpiled to DuckDB")
3506            this = this.this
3507
3508        inner = this.this if isinstance(this, exp.Order) else this
3509
3510        return self.func(
3511            "FLATTEN",
3512            exp.Filter(
3513                this=exp.ArrayAgg(this=this),
3514                expression=exp.Where(this=inner.copy().is_(exp.null()).not_()),
3515            ),
3516        )
def arrayunionagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayUnionAgg) -> str:
3518    def arrayunionagg_sql(self, expression: exp.ArrayUnionAgg) -> str:
3519        self.unsupported("ARRAY_UNION_AGG is not supported in DuckDB")
3520        return self.function_fallback_sql(expression)
def arraydistinct_sql(self, expression: sqlglot.expressions.array.ArrayDistinct) -> str:
3522    def arraydistinct_sql(self, expression: exp.ArrayDistinct) -> str:
3523        arr = expression.this
3524        func = self.func("LIST_DISTINCT", arr)
3525
3526        if expression.args.get("check_null"):
3527            add_null_to_array = exp.func(
3528                "LIST_APPEND", exp.func("LIST_DISTINCT", exp.ArrayCompact(this=arr)), exp.Null()
3529            )
3530            return self.sql(
3531                exp.If(
3532                    this=exp.NEQ(
3533                        this=exp.ArraySize(this=arr), expression=exp.func("LIST_COUNT", arr)
3534                    ),
3535                    true=add_null_to_array,
3536                    false=func,
3537                )
3538            )
3539
3540        return func
def arrayintersect_sql(self, expression: sqlglot.expressions.array.ArrayIntersect) -> str:
3542    def arrayintersect_sql(self, expression: exp.ArrayIntersect) -> str:
3543        if expression.args.get("is_multiset") and len(expression.expressions) == 2:
3544            return self._array_bag_sql(
3545                self.ARRAY_INTERSECTION_CONDITION,
3546                expression.expressions[0],
3547                expression.expressions[1],
3548            )
3549        return self.function_fallback_sql(expression)
def arrayexcept_sql(self, expression: sqlglot.expressions.array.ArrayExcept) -> str:
3551    def arrayexcept_sql(self, expression: exp.ArrayExcept) -> str:
3552        arr1, arr2 = expression.this, expression.expression
3553        if expression.args.get("is_multiset"):
3554            return self._array_bag_sql(self.ARRAY_EXCEPT_CONDITION, arr1, arr2)
3555        return self.sql(
3556            exp.replace_placeholders(self.ARRAY_EXCEPT_SET_TEMPLATE, arr1=arr1, arr2=arr2)
3557        )
def arrayslice_sql(self, expression: sqlglot.expressions.array.ArraySlice) -> str:
3559    def arrayslice_sql(self, expression: exp.ArraySlice) -> str:
3560        """
3561        Transpiles Snowflake's ARRAY_SLICE (0-indexed, exclusive end) to DuckDB's
3562        ARRAY_SLICE (1-indexed, inclusive end) by wrapping start and end in CASE
3563        expressions that adjust the index at query time:
3564          - start: CASE WHEN start >= 0 THEN start + 1 ELSE start END
3565          - end:   CASE WHEN end < 0 THEN end - 1 ELSE end END
3566        """
3567        start, end = expression.args.get("start"), expression.args.get("end")
3568
3569        if expression.args.get("zero_based"):
3570            if start is not None:
3571                start = (
3572                    exp.case()
3573                    .when(
3574                        exp.GTE(this=start.copy(), expression=exp.Literal.number(0)),
3575                        exp.Add(this=start.copy(), expression=exp.Literal.number(1)),
3576                    )
3577                    .else_(start)
3578                )
3579            if end is not None:
3580                end = (
3581                    exp.case()
3582                    .when(
3583                        exp.LT(this=end.copy(), expression=exp.Literal.number(0)),
3584                        exp.Sub(this=end.copy(), expression=exp.Literal.number(1)),
3585                    )
3586                    .else_(end)
3587                )
3588
3589        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:
3591    def arrayszip_sql(self, expression: exp.ArraysZip) -> str:
3592        args = expression.expressions
3593
3594        if not args:
3595            # Return [{}] - using MAP([], []) since DuckDB can't represent empty structs
3596            return self.sql(exp.array(exp.Map(keys=exp.array(), values=exp.array())))
3597
3598        # Build placeholder values for template
3599        lengths = [exp.Length(this=arg) for arg in args]
3600        max_len = (
3601            lengths[0]
3602            if len(lengths) == 1
3603            else exp.Greatest(this=lengths[0], expressions=lengths[1:])
3604        )
3605
3606        # Empty struct with same schema: {'$1': NULL, '$2': NULL, ...}
3607        empty_struct = exp.func(
3608            "STRUCT",
3609            *[
3610                exp.PropertyEQ(this=exp.Literal.string(f"${i + 1}"), expression=exp.Null())
3611                for i in range(len(args))
3612            ],
3613        )
3614
3615        # Struct for transform: {'$1': COALESCE(arr1, [])[__i + 1], ...}
3616        # COALESCE wrapping handles NULL arrays - prevents invalid NULL[i] syntax
3617        index = exp.column("__i") + 1
3618        transform_struct = exp.func(
3619            "STRUCT",
3620            *[
3621                exp.PropertyEQ(
3622                    this=exp.Literal.string(f"${i + 1}"),
3623                    expression=exp.func("COALESCE", arg, exp.array())[index],
3624                )
3625                for i, arg in enumerate(args)
3626            ],
3627        )
3628
3629        result = exp.replace_placeholders(
3630            self.ARRAYS_ZIP_TEMPLATE.copy(),
3631            null_check=exp.or_(*[arg.is_(exp.Null()) for arg in args]),
3632            all_empty_check=exp.and_(
3633                *[
3634                    exp.EQ(this=exp.Length(this=arg), expression=exp.Literal.number(0))
3635                    for arg in args
3636                ]
3637            ),
3638            empty_struct=empty_struct,
3639            max_len=max_len,
3640            transform_struct=transform_struct,
3641        )
3642        return self.sql(result)
def lower_sql(self, expression: sqlglot.expressions.string.Lower) -> str:
3644    def lower_sql(self, expression: exp.Lower) -> str:
3645        result_sql = self.func("LOWER", _cast_to_varchar(expression.this))
3646        return _gen_with_cast_to_blob(self, expression, result_sql)
def upper_sql(self, expression: sqlglot.expressions.string.Upper) -> str:
3648    def upper_sql(self, expression: exp.Upper) -> str:
3649        result_sql = self.func("UPPER", _cast_to_varchar(expression.this))
3650        return _gen_with_cast_to_blob(self, expression, result_sql)
def reverse_sql(self, expression: sqlglot.expressions.string.Reverse) -> str:
3652    def reverse_sql(self, expression: exp.Reverse) -> str:
3653        result_sql = self.func("REVERSE", _cast_to_varchar(expression.this))
3654        return _gen_with_cast_to_blob(self, expression, result_sql)
def left_sql(self, expression: sqlglot.expressions.string.Left) -> str:
3680    def left_sql(self, expression: exp.Left) -> str:
3681        return self._left_right_sql(expression, "LEFT")
def right_sql(self, expression: sqlglot.expressions.string.Right) -> str:
3683    def right_sql(self, expression: exp.Right) -> str:
3684        return self._left_right_sql(expression, "RIGHT")
def rtrimmedlength_sql(self, expression: sqlglot.expressions.string.RtrimmedLength) -> str:
3686    def rtrimmedlength_sql(self, expression: exp.RtrimmedLength) -> str:
3687        return self.func("LENGTH", exp.Trim(this=expression.this, position="TRAILING"))
def stuff_sql(self, expression: sqlglot.expressions.string.Stuff) -> str:
3689    def stuff_sql(self, expression: exp.Stuff) -> str:
3690        base = expression.this
3691        start = expression.args["start"]
3692        length = expression.args["length"]
3693        insertion = expression.expression
3694        is_binary = _is_binary(base)
3695
3696        if is_binary:
3697            # DuckDB's SUBSTRING doesn't accept BLOB; operate on the HEX string instead
3698            # (each byte = 2 hex chars), then UNHEX back to BLOB
3699            base = exp.Hex(this=base)
3700            insertion = exp.Hex(this=insertion)
3701            left = exp.Substring(
3702                this=base.copy(),
3703                start=exp.Literal.number(1),
3704                length=(start.copy() - exp.Literal.number(1)) * exp.Literal.number(2),
3705            )
3706            right = exp.Substring(
3707                this=base.copy(),
3708                start=((start + length) - exp.Literal.number(1)) * exp.Literal.number(2)
3709                + exp.Literal.number(1),
3710            )
3711        else:
3712            left = exp.Substring(
3713                this=base.copy(),
3714                start=exp.Literal.number(1),
3715                length=start.copy() - exp.Literal.number(1),
3716            )
3717            right = exp.Substring(this=base.copy(), start=start + length)
3718        result: exp.Expr = exp.DPipe(
3719            this=exp.DPipe(this=left, expression=insertion), expression=right
3720        )
3721
3722        if is_binary:
3723            result = exp.Unhex(this=result)
3724
3725        return self.sql(result)
def rand_sql(self, expression: sqlglot.expressions.functions.Rand) -> str:
3727    def rand_sql(self, expression: exp.Rand) -> str:
3728        seed = expression.this
3729        if seed is not None:
3730            self.unsupported("RANDOM with seed is not supported in DuckDB")
3731
3732        lower = expression.args.get("lower")
3733        upper = expression.args.get("upper")
3734
3735        if lower and upper:
3736            # scale DuckDB's [0,1) to the specified range
3737            range_size = exp.paren(upper - lower)
3738            scaled = exp.Add(this=lower, expression=exp.func("random") * range_size)
3739
3740            # For now we assume that if bounds are set, return type is BIGINT. Snowflake/Teradata
3741            result = exp.cast(scaled, exp.DType.BIGINT)
3742            return self.sql(result)
3743
3744        # Default DuckDB behavior - just return RANDOM() as float
3745        return "RANDOM()"
def bytelength_sql(self, expression: sqlglot.expressions.string.ByteLength) -> str:
3747    def bytelength_sql(self, expression: exp.ByteLength) -> str:
3748        arg = expression.this
3749
3750        # Check if it's a text type (handles both literals and annotated expressions)
3751        if arg.is_type(*exp.DataType.TEXT_TYPES):
3752            return self.func("OCTET_LENGTH", exp.Encode(this=arg))
3753
3754        # Default: pass through as-is (conservative for DuckDB, handles binary and unannotated)
3755        return self.func("OCTET_LENGTH", arg)
def base64encode_sql(self, expression: sqlglot.expressions.string.Base64Encode) -> str:
3757    def base64encode_sql(self, expression: exp.Base64Encode) -> str:
3758        # DuckDB TO_BASE64 requires BLOB input
3759        # Snowflake BASE64_ENCODE accepts both VARCHAR and BINARY - for VARCHAR it implicitly
3760        # encodes UTF-8 bytes. We add ENCODE unless the input is a binary type.
3761        result = expression.this
3762
3763        # Check if input is a string type - ENCODE only accepts VARCHAR
3764        if result.is_type(*exp.DataType.TEXT_TYPES):
3765            result = exp.Encode(this=result)
3766
3767        result = exp.ToBase64(this=result)
3768
3769        max_line_length = expression.args.get("max_line_length")
3770        alphabet = expression.args.get("alphabet")
3771
3772        # Handle custom alphabet by replacing standard chars with custom ones
3773        result = _apply_base64_alphabet_replacements(result, alphabet)
3774
3775        # Handle max_line_length by inserting newlines every N characters
3776        line_length = (
3777            t.cast(int, max_line_length.to_py())
3778            if isinstance(max_line_length, exp.Literal) and max_line_length.is_number
3779            else 0
3780        )
3781        if line_length > 0:
3782            newline = exp.Chr(expressions=[exp.Literal.number(10)])
3783            result = exp.Trim(
3784                this=exp.RegexpReplace(
3785                    this=result,
3786                    expression=exp.Literal.string(f"(.{{{line_length}}})"),
3787                    replacement=exp.Concat(expressions=[exp.Literal.string("\\1"), newline.copy()]),
3788                ),
3789                expression=newline,
3790                position="TRAILING",
3791            )
3792
3793        return self.sql(result)
def hex_sql(self, expression: sqlglot.expressions.string.Hex) -> str:
3795    def hex_sql(self, expression: exp.Hex) -> str:
3796        case = expression.args.get("case")
3797
3798        if not case:
3799            return self.func("HEX", expression.this)
3800
3801        hex_expr = exp.Hex(this=expression.this)
3802        return self.sql(
3803            exp.case()
3804            .when(case.is_(exp.null()), exp.null())
3805            .when(case.copy().eq(0), exp.Lower(this=hex_expr.copy()))
3806            .else_(hex_expr)
3807        )
def replace_sql(self, expression: sqlglot.expressions.string.Replace) -> str:
3809    def replace_sql(self, expression: exp.Replace) -> str:
3810        result_sql = self.func(
3811            "REPLACE",
3812            _cast_to_varchar(expression.this),
3813            _cast_to_varchar(expression.expression),
3814            _cast_to_varchar(expression.args.get("replacement")),
3815        )
3816        return _gen_with_cast_to_blob(self, expression, result_sql)
def bitwisexor_sql(self, expression: sqlglot.expressions.core.BitwiseXor) -> str:
3823    def bitwisexor_sql(self, expression: exp.BitwiseXor) -> str:
3824        _prepare_binary_bitwise_args(expression)
3825        result_sql = self.func("XOR", expression.this, expression.expression)
3826        return _gen_with_cast_to_blob(self, expression, result_sql)
def objectinsert_sql(self, expression: sqlglot.expressions.json.ObjectInsert) -> str:
3828    def objectinsert_sql(self, expression: exp.ObjectInsert) -> str:
3829        this = expression.this
3830        key = expression.args.get("key")
3831        key_sql = key.name if isinstance(key, exp.Expr) else ""
3832        value_sql = self.sql(expression, "value")
3833
3834        kv_sql = f"{key_sql} := {value_sql}"
3835
3836        # If the input struct is empty e.g. transpiling OBJECT_INSERT(OBJECT_CONSTRUCT(), key, value) from Snowflake
3837        # then we can generate STRUCT_PACK which will build it since STRUCT_INSERT({}, key := value) is not valid DuckDB
3838        if isinstance(this, exp.Struct) and not this.expressions:
3839            return self.func("STRUCT_PACK", kv_sql)
3840
3841        return self.func("STRUCT_INSERT", this, kv_sql)
def mapcat_sql(self, expression: sqlglot.expressions.array.MapCat) -> str:
3843    def mapcat_sql(self, expression: exp.MapCat) -> str:
3844        result = exp.replace_placeholders(
3845            self.MAPCAT_TEMPLATE.copy(),
3846            map1=expression.this,
3847            map2=expression.expression,
3848        )
3849        return self.sql(result)
def mapcontainskey_sql(self, expression: sqlglot.expressions.array.MapContainsKey) -> str:
3851    def mapcontainskey_sql(self, expression: exp.MapContainsKey) -> str:
3852        return self.func(
3853            "ARRAY_CONTAINS", exp.func("MAP_KEYS", expression.args["key"]), expression.this
3854        )
def mapdelete_sql(self, expression: sqlglot.expressions.array.MapDelete) -> str:
3856    def mapdelete_sql(self, expression: exp.MapDelete) -> str:
3857        map_arg = expression.this
3858        keys_to_delete = expression.expressions
3859
3860        x_dot_key = exp.Dot(this=exp.to_identifier("x"), expression=exp.to_identifier("key"))
3861
3862        lambda_expr = exp.Lambda(
3863            this=exp.In(this=x_dot_key, expressions=keys_to_delete).not_(),
3864            expressions=[exp.to_identifier("x")],
3865        )
3866        result = exp.func(
3867            "MAP_FROM_ENTRIES",
3868            exp.ArrayFilter(this=exp.func("MAP_ENTRIES", map_arg), expression=lambda_expr),
3869        )
3870        return self.sql(result)
def mappick_sql(self, expression: sqlglot.expressions.array.MapPick) -> str:
3872    def mappick_sql(self, expression: exp.MapPick) -> str:
3873        map_arg = expression.this
3874        keys_to_pick = expression.expressions
3875
3876        x_dot_key = exp.Dot(this=exp.to_identifier("x"), expression=exp.to_identifier("key"))
3877
3878        if len(keys_to_pick) == 1 and keys_to_pick[0].is_type(exp.DType.ARRAY):
3879            lambda_expr = exp.Lambda(
3880                this=exp.func("ARRAY_CONTAINS", keys_to_pick[0], x_dot_key),
3881                expressions=[exp.to_identifier("x")],
3882            )
3883        else:
3884            lambda_expr = exp.Lambda(
3885                this=exp.In(this=x_dot_key, expressions=keys_to_pick),
3886                expressions=[exp.to_identifier("x")],
3887            )
3888
3889        result = exp.func(
3890            "MAP_FROM_ENTRIES",
3891            exp.func("LIST_FILTER", exp.func("MAP_ENTRIES", map_arg), lambda_expr),
3892        )
3893        return self.sql(result)
def mapsize_sql(self, expression: sqlglot.expressions.array.MapSize) -> str:
3895    def mapsize_sql(self, expression: exp.MapSize) -> str:
3896        return self.func("CARDINALITY", expression.this)
@unsupported_args('update_flag')
def mapinsert_sql(self, expression: sqlglot.expressions.array.MapInsert) -> str:
3898    @unsupported_args("update_flag")
3899    def mapinsert_sql(self, expression: exp.MapInsert) -> str:
3900        map_arg = expression.this
3901        key = expression.args.get("key")
3902        value = expression.args.get("value")
3903
3904        map_type = map_arg.type
3905
3906        if value is not None:
3907            if map_type and map_type.expressions and len(map_type.expressions) > 1:
3908                # Extract the value type from MAP(key_type, value_type)
3909                value_type = map_type.expressions[1]
3910                # Cast value to match the map's value type to avoid type conflicts
3911                value = exp.cast(value, value_type)
3912            # else: polymorphic MAP case - no type parameters available, use value as-is
3913
3914        # Create a single-entry map for the new key-value pair
3915        new_entry_struct = exp.Struct(expressions=[exp.PropertyEQ(this=key, expression=value)])
3916        new_entry: exp.Expression = exp.ToMap(this=new_entry_struct)
3917
3918        # Use MAP_CONCAT to merge the original map with the new entry
3919        # This automatically handles both insert and update cases
3920        result = exp.func("MAP_CONCAT", map_arg, new_entry)
3921
3922        return self.sql(result)
def startswith_sql(self, expression: sqlglot.expressions.string.StartsWith) -> str:
3924    def startswith_sql(self, expression: exp.StartsWith) -> str:
3925        return self.func(
3926            "STARTS_WITH",
3927            _cast_to_varchar(expression.this),
3928            _cast_to_varchar(expression.expression),
3929        )
def space_sql(self, expression: sqlglot.expressions.string.Space) -> str:
3931    def space_sql(self, expression: exp.Space) -> str:
3932        # DuckDB's REPEAT requires BIGINT for the count parameter
3933        return self.sql(
3934            exp.Repeat(
3935                this=exp.Literal.string(" "),
3936                times=exp.cast(expression.this, exp.DType.BIGINT),
3937            )
3938        )
def tablefromrows_sql(self, expression: sqlglot.expressions.query.TableFromRows) -> str:
3940    def tablefromrows_sql(self, expression: exp.TableFromRows) -> str:
3941        # For GENERATOR, unwrap TABLE() - just emit the Generator (becomes RANGE)
3942        if isinstance(expression.this, exp.Generator):
3943            # Preserve alias, joins, and other table-level args
3944            table = exp.Table(
3945                this=expression.this,
3946                alias=expression.args.get("alias"),
3947                joins=expression.args.get("joins"),
3948            )
3949            return self.sql(table)
3950
3951        return super().tablefromrows_sql(expression)
def unnest_sql(self, expression: sqlglot.expressions.array.Unnest) -> str:
3953    def unnest_sql(self, expression: exp.Unnest) -> str:
3954        explode_array = expression.args.get("explode_array")
3955        if explode_array:
3956            # In BigQuery, UNNESTing a nested array leads to explosion of the top-level array & struct
3957            # This is transpiled to DDB by transforming "FROM UNNEST(...)" to "FROM (SELECT UNNEST(..., max_depth => 2))"
3958            expression.expressions.append(
3959                exp.Kwarg(this=exp.var("max_depth"), expression=exp.Literal.number(2))
3960            )
3961
3962            # If BQ's UNNEST is aliased, we transform it from a column alias to a table alias in DDB
3963            alias = expression.args.get("alias")
3964            if isinstance(alias, exp.TableAlias):
3965                expression.set("alias", None)
3966                if alias.columns:
3967                    alias = exp.TableAlias(this=seq_get(alias.columns, 0))
3968
3969            unnest_sql = super().unnest_sql(expression)
3970            select = exp.Select(expressions=[unnest_sql]).subquery(alias)
3971            return self.sql(select)
3972
3973        return super().unnest_sql(expression)
def arrayagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayAgg) -> str:
3975    def arrayagg_sql(self, expression: exp.ArrayAgg) -> str:
3976        if isinstance(expression.this, exp.Limit):
3977            self.unsupported("LIMIT inside ARRAY_AGG is not supported in DuckDB")
3978
3979        return super().arrayagg_sql(expression)
def ignorenulls_sql(self, expression: sqlglot.expressions.core.IgnoreNulls) -> str:
3981    def ignorenulls_sql(self, expression: exp.IgnoreNulls) -> str:
3982        this = expression.this
3983
3984        if isinstance(this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
3985            # DuckDB should render IGNORE NULLS only for the general-purpose
3986            # window functions that accept it e.g. FIRST_VALUE(... IGNORE NULLS) OVER (...)
3987            return super().ignorenulls_sql(expression)
3988
3989        # For ARRAY_AGG(expr IGNORE NULLS ...), convert IGNORE NULLS to a
3990        # FILTER(WHERE expr IS NOT NULL) clause by setting nulls_excluded on
3991        # the ArrayAgg.  The existing _add_arrayagg_null_filter method will
3992        # emit the FILTER clause during arrayagg_sql / withingroup_sql.
3993        if isinstance(this, exp.ArrayAgg):
3994            this.set("nulls_excluded", True)
3995            return self.sql(this)
3996
3997        if isinstance(this, exp.First):
3998            this = exp.AnyValue(this=this.this)
3999
4000        if not isinstance(this, (exp.AnyValue, exp.ApproxQuantiles)):
4001            self.unsupported("IGNORE NULLS is not supported for non-window functions.")
4002
4003        return self.sql(this)
def split_sql(self, expression: sqlglot.expressions.string.Split) -> str:
4005    def split_sql(self, expression: exp.Split) -> str:
4006        base_func = exp.func("STR_SPLIT", expression.this, expression.expression)
4007
4008        case_expr = exp.case().else_(base_func)
4009        needs_case = False
4010
4011        if expression.args.get("null_returns_null"):
4012            case_expr = case_expr.when(expression.expression.is_(exp.null()), exp.null())
4013            needs_case = True
4014
4015        if expression.args.get("empty_delimiter_returns_whole"):
4016            # When delimiter is empty string, return input string as single array element
4017            array_with_input = exp.array(expression.this)
4018            case_expr = case_expr.when(
4019                expression.expression.eq(exp.Literal.string("")), array_with_input
4020            )
4021            needs_case = True
4022
4023        return self.sql(case_expr if needs_case else base_func)
def splitpart_sql(self, expression: sqlglot.expressions.string.SplitPart) -> str:
4025    def splitpart_sql(self, expression: exp.SplitPart) -> str:
4026        string_arg = expression.this
4027        delimiter_arg = expression.args.get("delimiter")
4028        part_index_arg = expression.args.get("part_index")
4029
4030        if delimiter_arg and part_index_arg:
4031            # Handle Snowflake's "index 0 and 1 both return first element" behavior
4032            if expression.args.get("part_index_zero_as_one"):
4033                # Convert 0 to 1 for compatibility
4034
4035                part_index_arg = exp.Paren(
4036                    this=exp.case()
4037                    .when(part_index_arg.eq(exp.Literal.number("0")), exp.Literal.number("1"))
4038                    .else_(part_index_arg)
4039                )
4040
4041            # Use Anonymous to avoid recursion
4042            base_func_expr: exp.Expr = exp.Anonymous(
4043                this="SPLIT_PART", expressions=[string_arg, delimiter_arg, part_index_arg]
4044            )
4045            needs_case_transform = False
4046            case_expr = exp.case().else_(base_func_expr)
4047
4048            if expression.args.get("empty_delimiter_returns_whole"):
4049                # When delimiter is empty string:
4050                # - Return whole string if part_index is 1 or -1
4051                # - Return empty string otherwise
4052                empty_case = exp.Paren(
4053                    this=exp.case()
4054                    .when(
4055                        exp.or_(
4056                            part_index_arg.eq(exp.Literal.number("1")),
4057                            part_index_arg.eq(exp.Literal.number("-1")),
4058                        ),
4059                        string_arg,
4060                    )
4061                    .else_(exp.Literal.string(""))
4062                )
4063
4064                case_expr = case_expr.when(delimiter_arg.eq(exp.Literal.string("")), empty_case)
4065                needs_case_transform = True
4066
4067            """
4068            Output looks something like this:
4069
4070            CASE
4071            WHEN delimiter is '' THEN
4072                (
4073                    CASE
4074                    WHEN adjusted_part_index = 1 OR adjusted_part_index = -1 THEN input
4075                    ELSE '' END
4076                )
4077            ELSE SPLIT_PART(input, delimiter, adjusted_part_index)
4078            END
4079
4080            """
4081            return self.sql(case_expr if needs_case_transform else base_func_expr)
4082
4083        return self.function_fallback_sql(expression)
def respectnulls_sql(self, expression: sqlglot.expressions.core.RespectNulls) -> str:
4085    def respectnulls_sql(self, expression: exp.RespectNulls) -> str:
4086        if isinstance(expression.this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
4087            # DuckDB should render RESPECT NULLS only for the general-purpose
4088            # window functions that accept it e.g. FIRST_VALUE(... RESPECT NULLS) OVER (...)
4089            return super().respectnulls_sql(expression)
4090
4091        self.unsupported("RESPECT NULLS is not supported for non-window functions.")
4092        return self.sql(expression, "this")
def arraytostring_sql(self, expression: sqlglot.expressions.array.ArrayToString) -> str:
4094    def arraytostring_sql(self, expression: exp.ArrayToString) -> str:
4095        null = expression.args.get("null")
4096
4097        if expression.args.get("null_is_empty"):
4098            x = exp.to_identifier("x")
4099            list_transform = exp.Transform(
4100                this=expression.this.copy(),
4101                expression=exp.Lambda(
4102                    this=exp.Coalesce(
4103                        this=exp.cast(x, "TEXT"), expressions=[exp.Literal.string("")]
4104                    ),
4105                    expressions=[x],
4106                ),
4107            )
4108            array_to_string = exp.ArrayToString(
4109                this=list_transform, expression=expression.expression
4110            )
4111            if expression.args.get("null_delim_is_null"):
4112                return self.sql(
4113                    exp.case()
4114                    .when(expression.expression.copy().is_(exp.null()), exp.null())
4115                    .else_(array_to_string)
4116                )
4117            return self.sql(array_to_string)
4118
4119        if null:
4120            x = exp.to_identifier("x")
4121            return self.sql(
4122                exp.ArrayToString(
4123                    this=exp.Transform(
4124                        this=expression.this,
4125                        expression=exp.Lambda(
4126                            this=exp.Coalesce(this=x, expressions=[null]),
4127                            expressions=[x],
4128                        ),
4129                    ),
4130                    expression=expression.expression,
4131                )
4132            )
4133
4134        return self.func("ARRAY_TO_STRING", expression.this, expression.expression)
def concatws_sql(self, expression: sqlglot.expressions.string.ConcatWs) -> str:
4136    def concatws_sql(self, expression: exp.ConcatWs) -> str:
4137        # DuckDB-specific: handle binary types using DPipe (||) operator
4138        separator = seq_get(expression.expressions, 0)
4139        args = expression.expressions[1:]
4140
4141        if any(_is_binary(arg) for arg in [separator, *args]):
4142            result = args[0]
4143            for arg in args[1:]:
4144                result = exp.DPipe(
4145                    this=exp.DPipe(this=result, expression=separator), expression=arg
4146                )
4147            return self.sql(result)
4148
4149        return super().concatws_sql(expression)
def regexpextract_sql(self, expression: sqlglot.expressions.string.RegexpExtract) -> str:
4204    def regexpextract_sql(self, expression: exp.RegexpExtract) -> str:
4205        return self._regexp_extract_sql(expression)
def regexpextractall_sql(self, expression: sqlglot.expressions.string.RegexpExtractAll) -> str:
4207    def regexpextractall_sql(self, expression: exp.RegexpExtractAll) -> str:
4208        return self._regexp_extract_sql(expression)
def regexpinstr_sql(self, expression: sqlglot.expressions.string.RegexpInstr) -> str:
4210    def regexpinstr_sql(self, expression: exp.RegexpInstr) -> str:
4211        this = expression.this
4212        pattern = expression.expression
4213        position = expression.args.get("position")
4214        orig_occ = expression.args.get("occurrence")
4215        occurrence = orig_occ or exp.Literal.number(1)
4216        option = expression.args.get("option")
4217        parameters = expression.args.get("parameters")
4218
4219        validated_flags = self._validate_regexp_flags(parameters, supported_flags="ims")
4220        if validated_flags:
4221            pattern = exp.Concat(expressions=[exp.Literal.string(f"(?{validated_flags})"), pattern])
4222
4223        # Handle starting position offset
4224        pos_offset: exp.Expr = exp.Literal.number(0)
4225        if position and (not position.is_int or position.to_py() > 1):
4226            this = exp.Substring(this=this, start=position)
4227            pos_offset = position - exp.Literal.number(1)
4228
4229        # Helper: LIST_SUM(LIST_TRANSFORM(list[1:end], x -> LENGTH(x)))
4230        def sum_lengths(func_name: str, end: exp.Expr) -> exp.Expr:
4231            lst = exp.Bracket(
4232                this=exp.Anonymous(this=func_name, expressions=[this, pattern]),
4233                expressions=[exp.Slice(this=exp.Literal.number(1), expression=end)],
4234                offset=1,
4235            )
4236            transform = exp.Anonymous(
4237                this="LIST_TRANSFORM",
4238                expressions=[
4239                    lst,
4240                    exp.Lambda(
4241                        this=exp.Length(this=exp.to_identifier("x")),
4242                        expressions=[exp.to_identifier("x")],
4243                    ),
4244                ],
4245            )
4246            return exp.Coalesce(
4247                this=exp.Anonymous(this="LIST_SUM", expressions=[transform]),
4248                expressions=[exp.Literal.number(0)],
4249            )
4250
4251        # Position = 1 + sum(split_lengths[1:occ]) + sum(match_lengths[1:occ-1]) + offset
4252        base_pos: exp.Expr = (
4253            exp.Literal.number(1)
4254            + sum_lengths("STRING_SPLIT_REGEX", occurrence)
4255            + sum_lengths("REGEXP_EXTRACT_ALL", occurrence - exp.Literal.number(1))
4256            + pos_offset
4257        )
4258
4259        # option=1: add match length for end position
4260        if option and option.is_int and option.to_py() == 1:
4261            match_at_occ = exp.Bracket(
4262                this=exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern]),
4263                expressions=[occurrence],
4264                offset=1,
4265            )
4266            base_pos = base_pos + exp.Coalesce(
4267                this=exp.Length(this=match_at_occ), expressions=[exp.Literal.number(0)]
4268            )
4269
4270        # NULL checks for all provided arguments
4271        # .copy() is used strictly because .is_() alters the node's parent pointer, mutating the parsed AST
4272        null_args = [
4273            expression.this,
4274            expression.expression,
4275            position,
4276            orig_occ,
4277            option,
4278            parameters,
4279        ]
4280        null_checks = [arg.copy().is_(exp.Null()) for arg in null_args if arg]
4281
4282        matches = exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern])
4283
4284        return self.sql(
4285            exp.case()
4286            .when(exp.or_(*null_checks), exp.Null())
4287            .when(pattern.copy().eq(exp.Literal.string("")), exp.Literal.number(0))
4288            .when(exp.Length(this=matches) < occurrence, exp.Literal.number(0))
4289            .else_(base_pos)
4290        )
@unsupported_args('culture')
def numbertostr_sql(self, expression: sqlglot.expressions.string.NumberToStr) -> str:
4292    @unsupported_args("culture")
4293    def numbertostr_sql(self, expression: exp.NumberToStr) -> str:
4294        fmt = expression.args.get("format")
4295        if fmt and fmt.is_int:
4296            return self.func("FORMAT", f"'{{:,.{fmt.name}f}}'", expression.this)
4297
4298        self.unsupported("Only integer formats are supported by NumberToStr")
4299        return self.function_fallback_sql(expression)
def autoincrementcolumnconstraint_sql(self, _) -> str:
4301    def autoincrementcolumnconstraint_sql(self, _) -> str:
4302        self.unsupported("The AUTOINCREMENT column constraint is not supported by DuckDB")
4303        return ""
def aliases_sql(self, expression: sqlglot.expressions.core.Aliases) -> str:
4305    def aliases_sql(self, expression: exp.Aliases) -> str:
4306        this = expression.this
4307        if isinstance(this, exp.Posexplode):
4308            return self.posexplode_sql(this)
4309
4310        return super().aliases_sql(expression)
def posexplode_sql(self, expression: sqlglot.expressions.array.Posexplode) -> str:
4312    def posexplode_sql(self, expression: exp.Posexplode) -> str:
4313        this = expression.this
4314        parent = expression.parent
4315
4316        # The default Spark aliases are "pos" and "col", unless specified otherwise
4317        pos, col = exp.to_identifier("pos"), exp.to_identifier("col")
4318
4319        if isinstance(parent, exp.Aliases):
4320            # Column case: SELECT POSEXPLODE(col) [AS (a, b)]
4321            pos, col = parent.expressions
4322        elif isinstance(parent, exp.Table):
4323            # Table case: SELECT * FROM POSEXPLODE(col) [AS (a, b)]
4324            alias = parent.args.get("alias")
4325            if alias:
4326                pos, col = alias.columns or [pos, col]
4327                alias.pop()
4328
4329        # Translate POSEXPLODE to UNNEST + GENERATE_SUBSCRIPTS
4330        # Note: In Spark pos is 0-indexed, but in DuckDB it's 1-indexed, so we subtract 1 from GENERATE_SUBSCRIPTS
4331        unnest_sql = self.sql(exp.Unnest(expressions=[this], alias=col))
4332        gen_subscripts = self.sql(
4333            exp.Alias(
4334                this=exp.Anonymous(
4335                    this="GENERATE_SUBSCRIPTS", expressions=[this, exp.Literal.number(1)]
4336                )
4337                - exp.Literal.number(1),
4338                alias=pos,
4339            )
4340        )
4341
4342        posexplode_sql = self.format_args(gen_subscripts, unnest_sql)
4343
4344        if isinstance(parent, exp.From) or (parent and isinstance(parent.parent, exp.From)):
4345            # SELECT * FROM POSEXPLODE(col) -> SELECT * FROM (SELECT GENERATE_SUBSCRIPTS(...), UNNEST(...))
4346            return self.sql(exp.Subquery(this=exp.Select(expressions=[posexplode_sql])))
4347
4348        return posexplode_sql
def addmonths_sql(self, expression: sqlglot.expressions.temporal.AddMonths) -> str:
4350    def addmonths_sql(self, expression: exp.AddMonths) -> str:
4351        """
4352        Handles three key issues:
4353        1. Float/decimal months: e.g., Snowflake rounds, whereas DuckDB INTERVAL requires integers
4354        2. End-of-month preservation: If input is last day of month, result is last day of result month
4355        3. Type preservation: Maintains DATE/TIMESTAMPTZ types (DuckDB defaults to TIMESTAMP)
4356        """
4357        from sqlglot.optimizer.annotate_types import annotate_types
4358
4359        this = expression.this
4360        if not this.type:
4361            this = annotate_types(this, dialect=self.dialect)
4362
4363        if this.is_type(*exp.DataType.TEXT_TYPES):
4364            this = exp.Cast(this=this, to=exp.DataType(this=exp.DType.TIMESTAMP))
4365
4366        # Detect float/decimal months to apply rounding (Snowflake behavior)
4367        # DuckDB INTERVAL syntax doesn't support non-integer expressions, so use TO_MONTHS
4368        months_expr = expression.expression
4369        if not months_expr.type:
4370            months_expr = annotate_types(months_expr, dialect=self.dialect)
4371
4372        # Build interval or to_months expression based on type
4373        # Float/decimal case: Round and use TO_MONTHS(CAST(ROUND(value) AS INT))
4374        interval_or_to_months = (
4375            exp.func("TO_MONTHS", exp.cast(exp.func("ROUND", months_expr), "INT"))
4376            if months_expr.is_type(
4377                exp.DType.FLOAT,
4378                exp.DType.DOUBLE,
4379                exp.DType.DECIMAL,
4380            )
4381            # Integer case: standard INTERVAL N MONTH syntax
4382            else exp.Interval(this=months_expr, unit=exp.var("MONTH"))
4383        )
4384
4385        date_add_expr = exp.Add(this=this, expression=interval_or_to_months)
4386
4387        # Apply end-of-month preservation if Snowflake flag is set
4388        # CASE WHEN LAST_DAY(date) = date THEN LAST_DAY(result) ELSE result END
4389        preserve_eom = expression.args.get("preserve_end_of_month")
4390        result_expr = (
4391            exp.case()
4392            .when(
4393                exp.EQ(this=exp.func("LAST_DAY", this), expression=this),
4394                exp.func("LAST_DAY", date_add_expr),
4395            )
4396            .else_(date_add_expr)
4397            if preserve_eom
4398            else date_add_expr
4399        )
4400
4401        # DuckDB's DATE_ADD function returns TIMESTAMP/DATETIME by default, even when the input is DATE
4402        # To match for example Snowflake's ADD_MONTHS behavior (which preserves the input type)
4403        # We need to cast the result back to the original type when the input is DATE or TIMESTAMPTZ
4404        # Example: ADD_MONTHS('2023-01-31'::date, 1) should return DATE, not TIMESTAMP
4405        if this.is_type(exp.DType.DATE, exp.DType.TIMESTAMPTZ):
4406            return self.sql(exp.Cast(this=result_expr, to=this.type))
4407        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:
4409    def format_sql(self, expression: exp.Format) -> str:
4410        if expression.name.lower() == "%s" and len(expression.expressions) == 1:
4411            return self.func("FORMAT", "'{}'", expression.expressions[0])
4412
4413        return self.function_fallback_sql(expression)
def hexstring_sql( self, expression: sqlglot.expressions.query.HexString, binary_function_repr: str | None = None) -> str:
4415    def hexstring_sql(
4416        self, expression: exp.HexString, binary_function_repr: str | None = None
4417    ) -> str:
4418        # UNHEX('FF') correctly produces blob \xFF in DuckDB
4419        return super().hexstring_sql(expression, binary_function_repr="UNHEX")
def datetrunc_sql(self, expression: sqlglot.expressions.temporal.DateTrunc) -> str:
4421    def datetrunc_sql(self, expression: exp.DateTrunc) -> str:
4422        unit = expression.args.get("unit")
4423        date = expression.this
4424
4425        week_start = _week_trunc_start_dow(unit)
4426        unit = unit_to_str(expression)
4427
4428        if week_start:
4429            result = self.sql(
4430                _build_week_trunc_expression(date, week_start, preserve_start_day=True)
4431            )
4432        else:
4433            result = self.func("DATE_TRUNC", unit, date)
4434
4435        if (
4436            expression.args.get("input_type_preserved")
4437            and date.is_type(*exp.DataType.TEMPORAL_TYPES)
4438            and not (is_date_unit(unit) and date.is_type(exp.DType.DATE))
4439        ):
4440            return self.sql(exp.Cast(this=result, to=date.type))
4441
4442        return result
def datetimetrunc_sql(self, expression: sqlglot.expressions.temporal.DatetimeTrunc) -> str:
4444    def datetimetrunc_sql(self, expression: exp.DatetimeTrunc) -> str:
4445        this = exp.cast(expression.this, exp.DType.DATETIME)
4446        week_start = _week_trunc_start_dow(expression.args.get("unit"))
4447        if week_start:
4448            return self.sql(
4449                _build_week_trunc_expression(
4450                    this, week_start, preserve_start_day=True, cast_to_date=False
4451                )
4452            )
4453
4454        return self.func("DATE_TRUNC", unit_to_str(expression), this)
def timestamptrunc_sql(self, expression: sqlglot.expressions.temporal.TimestampTrunc) -> str:
4456    def timestamptrunc_sql(self, expression: exp.TimestampTrunc) -> str:
4457        zone = expression.args.get("zone")
4458        timestamp = expression.this
4459        week_start = _week_trunc_start_dow(expression.args.get("unit"))
4460
4461        # The week start emulation below is exact, so avoid weekstart_unit_to_str's degrade warning
4462        unit = unit_to_str(expression) if week_start else weekstart_unit_to_str(self, expression)
4463        date_unit = is_date_unit(unit) or bool(week_start)
4464
4465        def _trunc_expr(this: exp.Expr) -> exp.Expr:
4466            if week_start:
4467                return _build_week_trunc_expression(
4468                    this, week_start, preserve_start_day=True, cast_to_date=False
4469                )
4470            return exp.func("DATE_TRUNC", unit, this)
4471
4472        if date_unit and zone:
4473            # BigQuery's TIMESTAMP_TRUNC with timezone truncates in the target timezone and returns as UTC.
4474            # Double AT TIME ZONE needed for BigQuery compatibility:
4475            # 1. First AT TIME ZONE: ensures truncation happens in the target timezone
4476            # 2. Second AT TIME ZONE: converts the DATE result back to TIMESTAMPTZ (preserving time component)
4477            timestamp = exp.AtTimeZone(this=timestamp, zone=zone)
4478            trunced = _trunc_expr(timestamp)
4479            if isinstance(trunced, exp.DateAdd):
4480                # Parenthesize so the trailing AT TIME ZONE binds to the whole shifted expression
4481                trunced = exp.Paren(this=trunced)
4482            return self.sql(exp.AtTimeZone(this=trunced, zone=zone))
4483
4484        result = self.sql(_trunc_expr(timestamp))
4485        if expression.args.get("input_type_preserved"):
4486            if timestamp.type and timestamp.is_type(exp.DType.TIME, exp.DType.TIMETZ):
4487                dummy_date = exp.Cast(
4488                    this=exp.Literal.string("1970-01-01"),
4489                    to=exp.DataType(this=exp.DType.DATE),
4490                )
4491                date_time = exp.Add(this=dummy_date, expression=timestamp)
4492                result = self.func("DATE_TRUNC", unit, date_time)
4493                return self.sql(exp.Cast(this=result, to=timestamp.type))
4494
4495            if timestamp.is_type(*exp.DataType.TEMPORAL_TYPES) and not (
4496                date_unit and timestamp.is_type(exp.DType.DATE)
4497            ):
4498                return self.sql(exp.Cast(this=result, to=timestamp.type))
4499
4500        return result
def trim_sql(self, expression: sqlglot.expressions.string.Trim) -> str:
4502    def trim_sql(self, expression: exp.Trim) -> str:
4503        expression.this.replace(_cast_to_varchar(expression.this))
4504        if expression.expression:
4505            expression.expression.replace(_cast_to_varchar(expression.expression))
4506
4507        result_sql = super().trim_sql(expression)
4508        return _gen_with_cast_to_blob(self, expression, result_sql)
def round_sql(self, expression: sqlglot.expressions.math.Round) -> str:
4510    def round_sql(self, expression: exp.Round) -> str:
4511        this = expression.this
4512        decimals = expression.args.get("decimals")
4513        truncate = expression.args.get("truncate")
4514
4515        # DuckDB requires the scale (decimals) argument to be an INT
4516        # Some dialects (e.g., Snowflake) allow non-integer scales and cast to an integer internally
4517        if decimals is not None and expression.args.get("casts_non_integer_decimals"):
4518            if not (decimals.is_int or decimals.is_type(*exp.DataType.INTEGER_TYPES)):
4519                decimals = exp.cast(decimals, exp.DType.INT)
4520
4521        func = "ROUND"
4522        if truncate:
4523            # BigQuery uses ROUND_HALF_EVEN; Snowflake uses HALF_TO_EVEN
4524            if truncate.this in ("ROUND_HALF_EVEN", "HALF_TO_EVEN"):
4525                func = "ROUND_EVEN"
4526                truncate = None
4527            # BigQuery uses ROUND_HALF_AWAY_FROM_ZERO; Snowflake uses HALF_AWAY_FROM_ZERO
4528            elif truncate.this in ("ROUND_HALF_AWAY_FROM_ZERO", "HALF_AWAY_FROM_ZERO"):
4529                truncate = None
4530
4531        return self.func(func, this, decimals, truncate)
def trycast_sql(self, expression: sqlglot.expressions.functions.TryCast) -> str:
4533    def trycast_sql(self, expression: exp.TryCast) -> str:
4534        to = expression.to
4535        to_type = to.this
4536        src = expression.this
4537
4538        if (
4539            expression.args.get("null_on_text_overflow")
4540            and to_type in exp.DataType.TEXT_TYPES
4541            and to.expressions
4542        ):
4543            return self.sql(
4544                exp.case()
4545                .when(
4546                    exp.LTE(this=exp.func("LENGTH", src), expression=to.expressions[0].this),
4547                    exp.cast(src, "TEXT"),
4548                )
4549                .else_(exp.Null())
4550            )
4551        elif to_type == exp.DType.DATE and expression.args.get("probe_date_format"):
4552            slash_strptime = exp.cast(
4553                exp.func("TRY_STRPTIME", src, exp.Literal.string(self._TRYCAST_DATE_SLASH_FMT)),
4554                "DATE",
4555            )
4556            mon_strptime = exp.cast(
4557                exp.func("TRY_STRPTIME", src, exp.Literal.string(self._TRYCAST_DATE_MON_FMT)),
4558                "DATE",
4559            )
4560            return self.sql(
4561                exp.case()
4562                .when(exp.func("CONTAINS", src, exp.Literal.string("/")), slash_strptime)
4563                .when(
4564                    exp.RegexpLike(this=src, expression=exp.Literal.string("[A-Za-z]")),
4565                    mon_strptime,
4566                )
4567                .else_(exp.TryCast(this=src, to=to))
4568            )
4569        elif (
4570            isinstance(to_type, exp.Interval)
4571            and (unit := to_type.unit)
4572            and expression.args.get("requires_string")
4573        ):
4574            interval_type = exp.DataType.build("INTERVAL")
4575            if isinstance(unit, exp.IntervalSpan):
4576                self.unsupported(
4577                    "TRY_CAST to INTERVAL with span (e.g. HOUR TO MINUTE) is not supported in DuckDB"
4578                )
4579                return self.sql(exp.TryCast(this=src, to=interval_type))
4580            return self.sql(
4581                exp.TryCast(
4582                    this=exp.DPipe(this=src, expression=exp.Literal.string(f" {unit.name}")),
4583                    to=interval_type,
4584                )
4585            )
4586
4587        return super().trycast_sql(expression)
def strtok_sql(self, expression: sqlglot.expressions.string.Strtok) -> str:
4589    def strtok_sql(self, expression: exp.Strtok) -> str:
4590        string_arg = expression.this
4591        delimiter_arg = expression.args.get("delimiter")
4592        part_index_arg = expression.args.get("part_index")
4593
4594        if delimiter_arg and part_index_arg:
4595            # Escape regex chars and build character class at runtime using REGEXP_REPLACE
4596            escaped_delimiter = exp.Anonymous(
4597                this="REGEXP_REPLACE",
4598                expressions=[
4599                    delimiter_arg,
4600                    exp.Literal.string(
4601                        r"([\[\]^.\-*+?(){}|$\\])"
4602                    ),  # Escape problematic regex chars
4603                    exp.Literal.string(
4604                        r"\\\1"
4605                    ),  # Replace with escaped version using $1 backreference
4606                    exp.Literal.string("g"),  # Global flag
4607                ],
4608            )
4609            # CASE WHEN delimiter = '' THEN '' ELSE CONCAT('[', escaped_delimiter, ']') END
4610            regex_pattern = (
4611                exp.case()
4612                .when(delimiter_arg.eq(exp.Literal.string("")), exp.Literal.string(""))
4613                .else_(
4614                    exp.func(
4615                        "CONCAT",
4616                        exp.Literal.string("["),
4617                        escaped_delimiter,
4618                        exp.Literal.string("]"),
4619                    )
4620                )
4621            )
4622
4623            # STRTOK skips empty strings, so we need to filter them out
4624            # LIST_FILTER(REGEXP_SPLIT_TO_ARRAY(string, pattern), x -> x != '')[index]
4625            split_array = exp.func("REGEXP_SPLIT_TO_ARRAY", string_arg, regex_pattern)
4626            x = exp.to_identifier("x")
4627            is_empty = x.eq(exp.Literal.string(""))
4628            filtered_array = exp.func(
4629                "LIST_FILTER",
4630                split_array,
4631                exp.Lambda(this=exp.not_(is_empty.copy()), expressions=[x.copy()]),
4632            )
4633            base_func = exp.Bracket(
4634                this=filtered_array,
4635                expressions=[part_index_arg],
4636                offset=1,
4637            )
4638
4639            # Use template with the built regex pattern
4640            result = exp.replace_placeholders(
4641                self.STRTOK_TEMPLATE.copy(),
4642                string=string_arg,
4643                delimiter=delimiter_arg,
4644                part_index=part_index_arg,
4645                base_func=base_func,
4646            )
4647
4648            return self.sql(result)
4649
4650        return self.function_fallback_sql(expression)
def strtoktoarray_sql(self, expression: sqlglot.expressions.array.StrtokToArray) -> str:
4652    def strtoktoarray_sql(self, expression: exp.StrtokToArray) -> str:
4653        string_arg = expression.this
4654        delimiter_arg = expression.args.get("expression") or exp.Literal.string(" ")
4655
4656        escaped = exp.RegexpReplace(
4657            this=delimiter_arg.copy(),
4658            expression=exp.Literal.string(r"([\[\]^.\-*+?(){}|$\\])"),
4659            replacement=exp.Literal.string(r"\\\1"),
4660            modifiers=exp.Literal.string("g"),
4661        )
4662        return self.sql(
4663            exp.replace_placeholders(
4664                self.STRTOK_TO_ARRAY_TEMPLATE.copy(),
4665                string=string_arg,
4666                delimiter=delimiter_arg,
4667                escaped=escaped,
4668            )
4669        )
def approxquantile_sql(self, expression: sqlglot.expressions.aggregate.ApproxQuantile) -> str:
4671    def approxquantile_sql(self, expression: exp.ApproxQuantile) -> str:
4672        result = self.func("APPROX_QUANTILE", expression.this, expression.args.get("quantile"))
4673
4674        # DuckDB returns integers for APPROX_QUANTILE, cast to DOUBLE if the expected type is a real type
4675        if expression.is_type(*exp.DataType.REAL_TYPES):
4676            result = f"CAST({result} AS DOUBLE)"
4677
4678        return result
def approxquantiles_sql(self, expression: sqlglot.expressions.aggregate.ApproxQuantiles) -> str:
4680    def approxquantiles_sql(self, expression: exp.ApproxQuantiles) -> str:
4681        """
4682        BigQuery's APPROX_QUANTILES(expr, n) returns an array of n+1 approximate quantile values
4683        dividing the input distribution into n equal-sized buckets.
4684
4685        Both BigQuery and DuckDB use approximate algorithms for quantile estimation, but BigQuery
4686        does not document the specific algorithm used so results may differ. DuckDB does not
4687        support RESPECT NULLS.
4688        """
4689        this = expression.this
4690        if isinstance(this, exp.Distinct):
4691            # APPROX_QUANTILES requires 2 args and DISTINCT node grabs both
4692            if len(this.expressions) < 2:
4693                self.unsupported("APPROX_QUANTILES requires a bucket count argument")
4694                return self.function_fallback_sql(expression)
4695            num_quantiles_expr = this.expressions[1].pop()
4696        else:
4697            num_quantiles_expr = expression.expression
4698
4699        if not isinstance(num_quantiles_expr, exp.Literal) or not num_quantiles_expr.is_int:
4700            self.unsupported("APPROX_QUANTILES bucket count must be a positive integer")
4701            return self.function_fallback_sql(expression)
4702
4703        num_quantiles = t.cast(int, num_quantiles_expr.to_py())
4704        if num_quantiles <= 0:
4705            self.unsupported("APPROX_QUANTILES bucket count must be a positive integer")
4706            return self.function_fallback_sql(expression)
4707
4708        quantiles = [
4709            exp.Literal.number(Decimal(i) / Decimal(num_quantiles))
4710            for i in range(num_quantiles + 1)
4711        ]
4712
4713        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:
4715    def jsonextractscalar_sql(self, expression: exp.JSONExtractScalar) -> str:
4716        if expression.args.get("scalar_only"):
4717            json_value = exp.JSONExtractScalar(
4718                this=rename_func("JSON_VALUE")(self, expression), expression="'$'"
4719            )
4720
4721            # `->>` binds looser than most operators, so the wrap logic needs the parent
4722            json_value.parent = expression.parent
4723            expression = json_value
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
SUPPORTS_GROUPING_SETS_AS_SUFFIX
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
SUPPORTS_ALTER_COLUMN_NULLABILITY
SUPPORTS_ALTER_COLUMN_IF_EXISTS
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
MOD_OPERATOR
MOD_PAREN_PARENT_TYPES
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
nthvalue_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
casestatement_sql
whileblock_sql
loopblock_sql
repeatblock_sql
leave_sql
iterate_sql
execute_sql
executesql_sql
altermodifysqlsecurity_sql
usingproperty_sql
renameindex_sql