Edit on GitHub

sqlglot.generators.duckdb

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

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

Arguments:
  • pretty: Whether to format the produced SQL string. Default: False.
  • identify: Determines when an identifier should be quoted. Possible values are: False (default): Never quote, except in cases where it's mandatory by the dialect. True: Always quote except for specials cases. 'safe': Only quote identifiers that are case insensitive.
  • normalize: Whether to normalize identifiers to lowercase. Default: False.
  • pad: The pad size in a formatted string. For example, this affects the indentation of a projection in a query, relative to its nesting level. Default: 2.
  • indent: The indentation size in a formatted string. For example, this affects the indentation of subqueries and filters under a WHERE clause. Default: 2.
  • normalize_functions: How to normalize function names. Possible values are: "upper" or True (default): Convert names to uppercase. "lower": Convert names to lowercase. False: Disables function name normalization.
  • unsupported_level: Determines the generator's behavior when it encounters unsupported expressions. Default ErrorLevel.WARN.
  • max_unsupported: Maximum number of unsupported messages to include in a raised UnsupportedError. This is only relevant if unsupported_level is ErrorLevel.RAISE. Default: 3
  • leading_comma: Whether the comma is leading or trailing in select expressions. This is only relevant when generating in pretty mode. Default: False
  • max_text_width: The max number of characters in a segment before creating new lines in pretty mode. The default is on the smaller end because the length only represents a segment and not the true line length. Default: 80
  • comments: Whether to preserve comments in the output SQL code. Default: True
PARAMETER_TOKEN = '$'
NAMED_PLACEHOLDER_TOKEN = '$'
JOIN_HINTS = False
TABLE_HINTS = False
QUERY_HINTS = False
LIMIT_FETCH = 'LIMIT'
STRUCT_DELIMITER = ('(', ')')
RENAME_TABLE_WITH_DB = False
NVL2_SUPPORTED = False
SEMI_ANTI_JOIN_WITH_SIDE = False
TABLESAMPLE_KEYWORDS = 'USING SAMPLE'
TABLESAMPLE_SEED_KEYWORD = 'REPEATABLE'
LAST_DAY_SUPPORTS_DATE_PART = False
JSON_KEY_VALUE_PAIR_SEP = ','
IGNORE_NULLS_IN_FUNC = True
IGNORE_NULLS_BEFORE_ORDER = False
JSON_PATH_BRACKETED_KEY_SUPPORTED = False
SUPPORTS_CREATE_TABLE_LIKE = False
MULTI_ARG_DISTINCT = False
CAN_IMPLEMENT_ARRAY_ANY = True
SUPPORTS_TO_NUMBER = False
SELECT_KINDS: tuple[str, ...] = ()
SUPPORTS_DECODE_CASE = False
SUPPORTS_DROP_ALTER_ICEBERG_PROPERTY = False
AFTER_HAVING_MODIFIER_TRANSFORMS = {'windows': <function <lambda>>, 'qualify': <function <lambda>>}
SUPPORTS_WINDOW_EXCLUDE = True
COPY_HAS_INTO_KEYWORD = False
STAR_EXCEPT = 'EXCLUDE'
PAD_FILL_PATTERN_IS_REQUIRED = True
ARRAY_SIZE_DIM_REQUIRED: bool | None = False
NORMALIZE_EXTRACT_DATE_PARTS = True
SUPPORTS_LIKE_QUANTIFIERS = False
HISTORICAL_DATA_POST_ALIAS = True
SET_ASSIGNMENT_REQUIRES_VARIABLE_KEYWORD = True
TRANSFORMS = {<class 'sqlglot.expressions.query.JSONPathKey'>: <function <lambda>>, <class 'sqlglot.expressions.query.JSONPathRoot'>: <function <lambda>>, <class 'sqlglot.expressions.query.JSONPathSubscript'>: <function <lambda>>, <class 'sqlglot.expressions.query.JSONPathWildcard'>: <function <lambda>>, <class 'sqlglot.expressions.core.Adjacent'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.AllowedValuesProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.AnalyzeColumns'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.AnalyzeWith'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.array.ArrayContainedBy'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.array.ArrayContainsAll'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.array.ArrayOverlaps'>: <function _array_overlaps_sql>, <class 'sqlglot.expressions.constraints.AssumeColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.AutoRefreshProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.BackupProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.CaseSpecificColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.CalledOnNullInputProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.math.Ceil'>: <function _ceil_floor>, <class 'sqlglot.expressions.constraints.CharacterSetColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.CharacterSetProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.ClusteredColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.CollateColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.CommentColumnConstraint'>: <function no_comment_column_constraint_sql>, <class 'sqlglot.expressions.functions.ConnectByRoot'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.string.ConvertToCharset'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.CopyGrantsProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.CredentialsProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.functions.CurrentCatalog'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.functions.SessionUser'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.DateFormatColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.DefaultColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.ApiProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.ApplicationProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.CatalogProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.ComputeProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.DatabaseProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.DynamicProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.EmptyProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.EncodeColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.EndStatement'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.EnviromentProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.HandlerProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.ParameterStyleProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.EphemeralColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.ExcludeColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.ExecuteAsProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.Except'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.ExternalProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.math.Floor'>: <function _ceil_floor>, <class 'sqlglot.expressions.query.Get'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.GlobalProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.HeapProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.HybridProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.IcebergProperty'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.properties.InheritsProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.InlineLengthColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.InputModelProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.Intersect'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.datatypes.IntervalSpan'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.functions.Int64'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.json.JSONBContainsAnyTopKeys'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.json.JSONBContainsAllTopKeys'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.json.JSONBDeleteAtPath'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.json.JSONBPathExists'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.json.JSONObject'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.json.JSONObjectAgg'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.properties.LanguageProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.LocationProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.LogProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.MaskingProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.MaterializedProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.functions.NetFunc'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.NetworkProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.NonClusteredColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.NoPrimaryIndexProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.NotForReplicationColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.OnCommitProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.OnProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.OnUpdateColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.core.Operator'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.OutputModelProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.core.ExtendsLeft'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.core.ExtendsRight'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.PathColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.PartitionedByBucket'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.PartitionByTruncate'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.core.PivotAny'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.array.PositionalColumn'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.ProjectionPolicyColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.InvisibleColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.ZeroFillColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.Put'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.RemoteWithConnectionModelProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.ReturnsProperty'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.properties.RowAccessProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.core.SafeFunc'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.SampleProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.SecureProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.SecurityIntegrationProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.SetConfigProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.SetProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.SettingsProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.SharingProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.SqlReadWriteProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.SqlSecurityProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.StabilityProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.Stream'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.StreamingTableProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.StrictProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.ddl.SwapTable'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.TableColumn'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.Tags'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.TemporaryProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.TitleColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.array.ToMap'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.ToTableProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.TransformModelProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.TransientProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.VirtualProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.ddl.TriggerExecute'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.Union'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.UnloggedProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.UsingTemplateProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.UsingData'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.UppercaseColumnConstraint'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.temporal.UtcDate'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.temporal.UtcTime'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.temporal.UtcTimestamp'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.query.Variadic'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.array.VarMap'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.ViewAttributeProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.VolatileProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.WithJournalTableProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.WithProcedureOptions'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.WithSchemaBindingProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.constraints.WithOperator'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.properties.ForceProperty'>: <function Generator.<lambda>>, <class 'sqlglot.expressions.aggregate.AnyValue'>: <function _anyvalue_sql>, <class 'sqlglot.expressions.core.ApproxDistinct'>: <function approx_count_distinct_sql>, <class 'sqlglot.expressions.math.Boolnot'>: <function _boolnot_sql>, <class 'sqlglot.expressions.math.Booland'>: <function _booland_sql>, <class 'sqlglot.expressions.math.Boolor'>: <function _boolor_sql>, <class 'sqlglot.expressions.array.Array'>: <function preprocess.<locals>._to_sql>, <class 'sqlglot.expressions.array.ArrayAppend'>: <function array_append_sql.<locals>._array_append_sql>, <class 'sqlglot.expressions.array.ArrayCompact'>: <function array_compact_sql>, <class 'sqlglot.expressions.array.ArrayConstructCompact'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.array.ArrayConcat'>: <function array_concat_sql.<locals>._array_concat_sql>, <class 'sqlglot.expressions.array.ArrayContains'>: <function _array_contains_sql>, <class 'sqlglot.expressions.array.ArrayFilter'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.array.ArrayInsert'>: <function _array_insert_sql>, <class 'sqlglot.expressions.array.ArrayPosition'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.array.ArrayRemoveAt'>: <function _array_remove_at_sql>, <class 'sqlglot.expressions.array.ArrayRemove'>: <function remove_from_array_using_filter>, <class 'sqlglot.expressions.array.ArraySort'>: <function _array_sort_sql>, <class 'sqlglot.expressions.array.ArrayPrepend'>: <function array_append_sql.<locals>._array_append_sql>, <class 'sqlglot.expressions.array.ArraySum'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.array.ArrayMax'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.array.ArrayMin'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.string.Base64DecodeBinary'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.string.Base64DecodeString'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.core.BitwiseAnd'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.math.BitwiseAndAgg'>: <function _bitwise_agg_sql>, <class 'sqlglot.expressions.math.BitwiseCount'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.core.BitwiseLeftShift'>: <function _bitshift_sql>, <class 'sqlglot.expressions.core.BitwiseOr'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.math.BitwiseOrAgg'>: <function _bitwise_agg_sql>, <class 'sqlglot.expressions.core.BitwiseRightShift'>: <function _bitshift_sql>, <class 'sqlglot.expressions.math.BitwiseXorAgg'>: <function _bitwise_agg_sql>, <class 'sqlglot.expressions.aggregate.Corr'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.math.CosineDistance'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.CurrentTime'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.functions.CurrentSchemas'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.CurrentTimestamp'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.functions.CurrentVersion'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.Localtime'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.DayOfMonth'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.DayOfWeek'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.DayOfWeekIso'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.DayOfYear'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.Dayname'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.Monthname'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _datatype_sql>, <class 'sqlglot.expressions.temporal.Date'>: <function _date_sql>, <class 'sqlglot.expressions.temporal.DateAdd'>: <function _date_delta_to_binary_interval_op.<locals>._duckdb_date_delta_sql>, <class 'sqlglot.expressions.temporal.DateFromParts'>: <function _date_from_parts_sql>, <class 'sqlglot.expressions.temporal.DateSub'>: <function _date_delta_to_binary_interval_op.<locals>._duckdb_date_delta_sql>, <class 'sqlglot.expressions.temporal.DateDiff'>: <function _date_diff_sql>, <class 'sqlglot.expressions.temporal.DateStrToDate'>: <function datestrtodate_sql>, <class 'sqlglot.expressions.temporal.Datetime'>: <function no_datetime_sql>, <class 'sqlglot.expressions.temporal.DatetimeDiff'>: <function _date_diff_sql>, <class 'sqlglot.expressions.temporal.DatetimeSub'>: <function _date_delta_to_binary_interval_op.<locals>._duckdb_date_delta_sql>, <class 'sqlglot.expressions.temporal.DatetimeAdd'>: <function _date_delta_to_binary_interval_op.<locals>._duckdb_date_delta_sql>, <class 'sqlglot.expressions.temporal.DateToDi'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.string.Decode'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.string.HexDecodeString'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.DiToDate'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.string.Encode'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.functions.EqualNull'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.math.EuclideanDistance'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.GenerateDateArray'>: <function _generate_datetime_array_sql>, <class 'sqlglot.expressions.array.GenerateSeries'>: <function generate_series_sql.<locals>._generate_series_sql>, <class 'sqlglot.expressions.temporal.GenerateTimestampArray'>: <function _generate_datetime_array_sql>, <class 'sqlglot.expressions.math.Getbit'>: <function getbit_sql>, <class 'sqlglot.expressions.aggregate.GroupConcat'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.array.Explode'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.core.IntDiv'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.math.IsInf'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.math.IsNan'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.functions.IsNullValue'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.functions.IsArray'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.json.JSONBExists'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.json.JSONExtract'>: <function _arrow_json_extract_sql>, <class 'sqlglot.expressions.json.JSONExtractArray'>: <function _json_extract_value_array_sql>, <class 'sqlglot.expressions.json.JSONFormat'>: <function _json_format_sql>, <class 'sqlglot.expressions.query.JSONValueArray'>: <function _json_extract_value_array_sql>, <class 'sqlglot.expressions.query.Lateral'>: <function _explode_to_unnest_sql>, <class 'sqlglot.expressions.aggregate.LogicalOr'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.aggregate.LogicalAnd'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.query.Select'>: <function preprocess.<locals>._to_sql>, <class 'sqlglot.expressions.functions.Seq1'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.functions.Seq2'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.functions.Seq4'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.functions.Seq8'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.math.BoolxorAgg'>: <function _boolxor_agg_sql>, <class 'sqlglot.expressions.temporal.MakeInterval'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.string.Initcap'>: <function _initcap_sql>, <class 'sqlglot.expressions.string.MD5Digest'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.string.SHA'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.string.SHA1Digest'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.string.SHA2'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.string.SHA2Digest'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.MonthsBetween'>: <function months_between_sql>, <class 'sqlglot.expressions.temporal.NextDay'>: <function _day_navigation_sql>, <class 'sqlglot.expressions.aggregate.PercentileCont'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.aggregate.PercentileDisc'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.query.Pivot'>: <function preprocess.<locals>._to_sql>, <class 'sqlglot.expressions.temporal.PreviousDay'>: <function _day_navigation_sql>, <class 'sqlglot.expressions.string.RegexpILike'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.string.RegexpSplit'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.aggregate.RegrValx'>: <function _regr_val_sql>, <class 'sqlglot.expressions.aggregate.RegrValy'>: <function _regr_val_sql>, <class 'sqlglot.expressions.query.Return'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.StrToUnix'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.array.Struct'>: <function _struct_sql>, <class 'sqlglot.expressions.array.Transform'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.TimeAdd'>: <function _date_delta_to_binary_interval_op.<locals>._duckdb_date_delta_sql>, <class 'sqlglot.expressions.temporal.TimeSub'>: <function _date_delta_to_binary_interval_op.<locals>._duckdb_date_delta_sql>, <class 'sqlglot.expressions.temporal.Time'>: <function no_time_sql>, <class 'sqlglot.expressions.temporal.TimeDiff'>: <function _timediff_sql>, <class 'sqlglot.expressions.temporal.Timestamp'>: <function no_timestamp_sql>, <class 'sqlglot.expressions.temporal.TimestampAdd'>: <function _date_delta_to_binary_interval_op.<locals>._duckdb_date_delta_sql>, <class 'sqlglot.expressions.temporal.TimestampDiff'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.TimestampSub'>: <function _date_delta_to_binary_interval_op.<locals>._duckdb_date_delta_sql>, <class 'sqlglot.expressions.temporal.TimeStrToDate'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.TimeStrToTime'>: <function timestrtotime_sql>, <class 'sqlglot.expressions.temporal.TimeStrToUnix'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.TimeToStr'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.functions.ToBoolean'>: <function _to_boolean_sql>, <class 'sqlglot.expressions.functions.ToVariant'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.TimeToUnix'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.TsOrDiToDi'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.TsOrDsAdd'>: <function _date_delta_to_binary_interval_op.<locals>._duckdb_date_delta_sql>, <class 'sqlglot.expressions.temporal.TsOrDsDiff'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.UnixMicros'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.UnixMillis'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.UnixSeconds'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.UnixToStr'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.UnixToTime'>: <function _unix_to_time_sql>, <class 'sqlglot.expressions.temporal.UnixToTimeStr'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.aggregate.VariancePop'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.WeekOfYear'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.YearOfWeek'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.temporal.YearOfWeekIso'>: <function DuckDBGenerator.<lambda>>, <class 'sqlglot.expressions.core.Xor'>: <function _xor_sql>, <class 'sqlglot.expressions.json.JSONBObjectAgg'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.DateBin'>: <function rename_func.<locals>.<lambda>>, <class 'sqlglot.expressions.temporal.LastDay'>: <function _last_day_sql>}
TYPE_MAPPING = {<DType.DATETIME2: 'DATETIME2'>: 'TIMESTAMP', <DType.NCHAR: 'NCHAR'>: 'TEXT', <DType.NVARCHAR: 'NVARCHAR'>: 'TEXT', <DType.MEDIUMTEXT: 'MEDIUMTEXT'>: 'TEXT', <DType.LONGTEXT: 'LONGTEXT'>: 'TEXT', <DType.TINYTEXT: 'TINYTEXT'>: 'TEXT', <DType.BLOB: 'BLOB'>: 'VARBINARY', <DType.MEDIUMBLOB: 'MEDIUMBLOB'>: 'BLOB', <DType.LONGBLOB: 'LONGBLOB'>: 'BLOB', <DType.TINYBLOB: 'TINYBLOB'>: 'BLOB', <DType.INET: 'INET'>: 'INET', <DType.ROWVERSION: 'ROWVERSION'>: 'BLOB', <DType.SMALLDATETIME: 'SMALLDATETIME'>: 'TIMESTAMP', <DType.BINARY: 'BINARY'>: 'BLOB', <DType.BPCHAR: 'BPCHAR'>: 'TEXT', <DType.CHAR: 'CHAR'>: 'TEXT', <DType.DATETIME: 'DATETIME'>: 'TIMESTAMP', <DType.DECFLOAT: 'DECFLOAT'>: 'DECIMAL', <DType.FLOAT: 'FLOAT'>: 'REAL', <DType.JSONB: 'JSONB'>: 'JSON', <DType.UINT: 'UINT'>: 'UINTEGER', <DType.VARBINARY: 'VARBINARY'>: 'BLOB', <DType.VARCHAR: 'VARCHAR'>: 'TEXT', <DType.TIMESTAMPLTZ: 'TIMESTAMPLTZ'>: 'TIMESTAMPTZ', <DType.TIMESTAMPNTZ: 'TIMESTAMPNTZ'>: 'TIMESTAMP', <DType.TIMESTAMP_S: 'TIMESTAMP_S'>: 'TIMESTAMP_S', <DType.TIMESTAMP_MS: 'TIMESTAMP_MS'>: 'TIMESTAMP_MS', <DType.TIMESTAMP_NS: 'TIMESTAMP_NS'>: 'TIMESTAMP_NS', <DType.BIGDECIMAL: 'BIGDECIMAL'>: 'DECIMAL'}
TYPE_PARAM_SETTINGS = {<DType.BIGDECIMAL: 'BIGDECIMAL'>: ((38, 5), (38, 38)), <DType.DECFLOAT: 'DECFLOAT'>: ((38, 5), (38, 38))}
RESERVED_KEYWORDS = {'default', 'where', 'check_p', 'leading', 'symmetric', 'any', 'union', 'all', 'returning', 'limit', 'some', 'do', 'not', 'current_timestamp', 'when', 'select', 'localtimestamp', 'variadic', 'to', 'user', 'deferrable', 'lateral_p', 'trailing', 'current_role', 'then', 'grant', 'constraint', 'order', 'null_p', 'group_p', 'as', 'offset', 'table', 'primary', 'end_p', 'both', 'cast', 'current_time', 'analyse', 'except', 'session_user', 'or', 'for', 'and', 'intersect', 'analyze', 'collate', 'localtime', 'current_user', 'initially', 'foreign', 'using', 'in_p', 'fetch', 'desc_p', 'asc_p', 'false_p', 'placing', 'from', 'column', 'references', 'case', 'distinct', 'create_p', 'array', 'true_p', 'into', 'having', 'current_catalog', 'on', 'else', 'asymmetric', 'current_date', 'only', 'unique', 'window', 'with'}
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 nthvalue_sql(self, expression: sqlglot.expressions.aggregate.NthValue) -> str:
2473    def nthvalue_sql(self, expression: exp.NthValue) -> str:
2474        from_first = expression.args.get("from_first", True)
2475        if not from_first:
2476            self.unsupported("DuckDB's NTH_VALUE doesn't support starting from the end ")
2477
2478        return self.function_fallback_sql(expression)
def randstr_sql(self, expression: sqlglot.expressions.functions.Randstr) -> str:
2480    def randstr_sql(self, expression: exp.Randstr) -> str:
2481        """
2482        Transpile Snowflake's RANDSTR to DuckDB equivalent using deterministic hash-based random.
2483        Uses a pre-parsed template with placeholders replaced by expression nodes.
2484
2485        RANDSTR(length, generator) generates a random string of specified length.
2486        - With numeric seed: Use HASH(i + seed) for deterministic output (same seed = same result)
2487        - With RANDOM(): Use RANDOM() in the hash for non-deterministic output
2488        - No generator: Use default seed value
2489        """
2490        length = expression.this
2491        generator = expression.args.get("generator")
2492
2493        if generator:
2494            if isinstance(generator, exp.Rand):
2495                # If it's RANDOM(), use its seed if available, otherwise use RANDOM() itself
2496                seed_value = generator.this or generator
2497            else:
2498                # Const/int or other expression - use as seed directly
2499                seed_value = generator
2500        else:
2501            # No generator specified, use default seed (arbitrary but deterministic)
2502            seed_value = exp.Literal.number(RANDSTR_SEED)
2503
2504        replacements = {"seed": seed_value, "length": length}
2505        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:
2507    @unsupported_args("finish")
2508    def reduce_sql(self, expression: exp.Reduce) -> str:
2509        array_arg = expression.this
2510        initial_value = expression.args.get("initial")
2511        merge_lambda = expression.args.get("merge")
2512
2513        if merge_lambda:
2514            merge_lambda.set("colon", True)
2515
2516        return self.func("list_reduce", array_arg, merge_lambda, initial_value)
def zipf_sql(self, expression: sqlglot.expressions.functions.Zipf) -> str:
2518    def zipf_sql(self, expression: exp.Zipf) -> str:
2519        """
2520        Transpile Snowflake's ZIPF to DuckDB using CDF-based inverse sampling.
2521        Uses a pre-parsed template with placeholders replaced by expression nodes.
2522        """
2523        s = expression.this
2524        n = expression.args["elementcount"]
2525        gen = expression.args["gen"]
2526
2527        if not isinstance(gen, exp.Rand):
2528            # (ABS(HASH(seed)) % 1000000) / 1000000.0
2529            random_expr: exp.Expr = exp.Div(
2530                this=exp.Paren(
2531                    this=exp.Mod(
2532                        this=exp.Abs(this=exp.Anonymous(this="HASH", expressions=[gen.copy()])),
2533                        expression=exp.Literal.number(1000000),
2534                    )
2535                ),
2536                expression=exp.Literal.number(1000000.0),
2537            )
2538        else:
2539            # Use RANDOM() for non-deterministic output
2540            random_expr = exp.Rand()
2541
2542        replacements = {"s": s, "n": n, "random_expr": random_expr}
2543        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:
2545    def tobinary_sql(self, expression: exp.ToBinary) -> str:
2546        """
2547        TO_BINARY and TRY_TO_BINARY transpilation:
2548        - 'HEX': TO_BINARY('48454C50', 'HEX') -> UNHEX('48454C50')
2549        - 'UTF-8': TO_BINARY('TEST', 'UTF-8') -> ENCODE('TEST')
2550        - 'BASE64': TO_BINARY('SEVMUA==', 'BASE64') -> FROM_BASE64('SEVMUA==')
2551
2552        For TRY_TO_BINARY (safe=True), wrap with TRY():
2553        - 'HEX': TRY_TO_BINARY('invalid', 'HEX') -> TRY(UNHEX('invalid'))
2554        """
2555        value = expression.this
2556        format_arg = expression.args.get("format")
2557        is_safe = expression.args.get("safe")
2558        is_binary = _is_binary(expression)
2559
2560        if not format_arg and not is_binary:
2561            func_name = "TRY_TO_BINARY" if is_safe else "TO_BINARY"
2562            return self.func(func_name, value)
2563
2564        # Snowflake defaults to HEX encoding when no format is specified
2565        fmt = format_arg.name.upper() if format_arg else "HEX"
2566
2567        if fmt in ("UTF-8", "UTF8"):
2568            # DuckDB ENCODE always uses UTF-8, no charset parameter needed
2569            result = self.func("ENCODE", value)
2570        elif fmt == "BASE64":
2571            result = self.func("FROM_BASE64", value)
2572        elif fmt == "HEX":
2573            result = self.func("UNHEX", value)
2574        else:
2575            if is_safe:
2576                return self.sql(exp.null())
2577            else:
2578                self.unsupported(f"format {fmt} is not supported")
2579                result = self.func("TO_BINARY", value)
2580        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:
2582    def tonumber_sql(self, expression: exp.ToNumber) -> str:
2583        fmt = expression.args.get("format")
2584        precision = expression.args.get("precision")
2585        scale = expression.args.get("scale")
2586
2587        if not fmt and precision and scale:
2588            return self.sql(
2589                exp.cast(
2590                    expression.this, f"DECIMAL({precision.name}, {scale.name})", dialect="duckdb"
2591                )
2592            )
2593
2594        return super().tonumber_sql(expression)
def generator_sql(self, expression: sqlglot.expressions.array.Generator) -> str:
2620    def generator_sql(self, expression: exp.Generator) -> str:
2621        # Transpile Snowflake GENERATOR to DuckDB range()
2622        rowcount = expression.args.get("rowcount")
2623        time_limit = expression.args.get("time_limit")
2624
2625        if time_limit:
2626            self.unsupported("GENERATOR TIMELIMIT parameter is not supported in DuckDB")
2627
2628        if not rowcount:
2629            self.unsupported("GENERATOR without ROWCOUNT is not supported in DuckDB")
2630            return self.func("range", exp.Literal.number(0))
2631
2632        return self.func("range", rowcount)
def greatest_sql(self, expression: sqlglot.expressions.functions.Greatest) -> str:
2634    def greatest_sql(self, expression: exp.Greatest) -> str:
2635        return self._greatest_least_sql(expression)
def least_sql(self, expression: sqlglot.expressions.functions.Least) -> str:
2637    def least_sql(self, expression: exp.Least) -> str:
2638        return self._greatest_least_sql(expression)
def lambda_sql( self, expression: sqlglot.expressions.query.Lambda, arrow_sep: str = '->', wrap: bool = True) -> str:
2640    def lambda_sql(self, expression: exp.Lambda, arrow_sep: str = "->", wrap: bool = True) -> str:
2641        if expression.args.get("colon"):
2642            prefix = "LAMBDA "
2643            arrow_sep = ":"
2644            wrap = False
2645        else:
2646            prefix = ""
2647
2648        lambda_sql = super().lambda_sql(expression, arrow_sep=arrow_sep, wrap=wrap)
2649        return f"{prefix}{lambda_sql}"
def show_sql(self, expression: sqlglot.expressions.ddl.Show) -> str:
2651    def show_sql(self, expression: exp.Show) -> str:
2652        from_ = self.sql(expression, "from_")
2653        from_ = f" FROM {from_}" if from_ else ""
2654        return f"SHOW {expression.name}{from_}"
def soundex_sql(self, expression: sqlglot.expressions.string.Soundex) -> str:
2656    def soundex_sql(self, expression: exp.Soundex) -> str:
2657        self.unsupported("SOUNDEX is not supported in DuckDB")
2658        return self.func("SOUNDEX", expression.this)
def sortarray_sql(self, expression: sqlglot.expressions.array.SortArray) -> str:
2660    def sortarray_sql(self, expression: exp.SortArray) -> str:
2661        arr = expression.this
2662        asc = expression.args.get("asc")
2663        nulls_first = expression.args.get("nulls_first")
2664
2665        if not isinstance(asc, exp.Boolean) and not isinstance(nulls_first, exp.Boolean):
2666            return self.func("LIST_SORT", arr, asc, nulls_first)
2667
2668        nulls_are_first = nulls_first == exp.true()
2669        nulls_first_sql = exp.Literal.string("NULLS FIRST") if nulls_are_first else None
2670
2671        if not isinstance(asc, exp.Boolean):
2672            return self.func("LIST_SORT", arr, asc, nulls_first_sql)
2673
2674        descending = asc == exp.false()
2675
2676        if not descending and not nulls_are_first:
2677            return self.func("LIST_SORT", arr)
2678        if not nulls_are_first:
2679            return self.func("ARRAY_REVERSE_SORT", arr)
2680        return self.func(
2681            "LIST_SORT",
2682            arr,
2683            exp.Literal.string("DESC" if descending else "ASC"),
2684            exp.Literal.string("NULLS FIRST"),
2685        )
def install_sql(self, expression: sqlglot.expressions.ddl.Install) -> str:
2687    def install_sql(self, expression: exp.Install) -> str:
2688        force = "FORCE " if expression.args.get("force") else ""
2689        this = self.sql(expression, "this")
2690        from_clause = expression.args.get("from_")
2691        from_clause = f" FROM {from_clause}" if from_clause else ""
2692        return f"{force}INSTALL {this}{from_clause}"
def approxtopk_sql(self, expression: sqlglot.expressions.aggregate.ApproxTopK) -> str:
2694    def approxtopk_sql(self, expression: exp.ApproxTopK) -> str:
2695        self.unsupported(
2696            "APPROX_TOP_K cannot be transpiled to DuckDB due to incompatible return types. "
2697        )
2698        return self.function_fallback_sql(expression)
def strposition_sql(self, expression: sqlglot.expressions.string.StrPosition) -> str:
2700    def strposition_sql(self, expression: exp.StrPosition) -> str:
2701        this = expression.this
2702        substr = expression.args.get("substr")
2703        position = expression.args.get("position")
2704
2705        # For BINARY/BLOB: DuckDB's STRPOS doesn't support BLOB types
2706        # Convert to HEX strings, use STRPOS, then convert hex position to byte position
2707        if _is_binary(this):
2708            # Build expression: STRPOS(HEX(haystack), HEX(needle))
2709            hex_strpos = exp.StrPosition(
2710                this=exp.Hex(this=this),
2711                substr=exp.Hex(this=substr),
2712            )
2713
2714            return self.sql(exp.cast((hex_strpos + 1) / 2, exp.DType.INT))
2715
2716        # For VARCHAR: handle clamp_position
2717        if expression.args.get("clamp_position") and position:
2718            expression = expression.copy()
2719            expression.set(
2720                "position",
2721                exp.If(
2722                    this=exp.LTE(this=position, expression=exp.Literal.number(0)),
2723                    true=exp.Literal.number(1),
2724                    false=position.copy(),
2725                ),
2726            )
2727
2728        return strposition_sql(self, expression)
def substring_sql(self, expression: sqlglot.expressions.string.Substring) -> str:
2730    def substring_sql(self, expression: exp.Substring) -> str:
2731        if expression.args.get("zero_start"):
2732            start = expression.args.get("start")
2733            length = expression.args.get("length")
2734
2735            if start := expression.args.get("start"):
2736                start = exp.If(this=start.eq(0), true=exp.Literal.number(1), false=start)
2737            if length := expression.args.get("length"):
2738                length = exp.If(this=length < 0, true=exp.Literal.number(0), false=length)
2739
2740            return self.func("SUBSTRING", expression.this, start, length)
2741
2742        return self.function_fallback_sql(expression)
def strtotime_sql(self, expression: sqlglot.expressions.temporal.StrToTime) -> str:
2744    def strtotime_sql(self, expression: exp.StrToTime) -> str:
2745        # Check if target_type requires TIMESTAMPTZ (for LTZ/TZ variants)
2746        target_type = expression.args.get("target_type")
2747        needs_tz = target_type and target_type.this in (
2748            exp.DType.TIMESTAMPLTZ,
2749            exp.DType.TIMESTAMPTZ,
2750        )
2751
2752        value, formatted_time = self._strptime_default_year(expression)
2753
2754        if expression.args.get("safe"):
2755            cast_type = exp.DType.TIMESTAMPTZ if needs_tz else exp.DType.TIMESTAMP
2756            return self.sql(exp.cast(self.func("TRY_STRPTIME", value, formatted_time), cast_type))
2757
2758        base_sql = self.func("STRPTIME", value, formatted_time)
2759        if needs_tz:
2760            return self.sql(
2761                exp.cast(
2762                    base_sql,
2763                    exp.DataType(this=exp.DType.TIMESTAMPTZ),
2764                )
2765            )
2766        return base_sql
def strtodate_sql(self, expression: sqlglot.expressions.temporal.StrToDate) -> str:
2768    def strtodate_sql(self, expression: exp.StrToDate) -> str:
2769        value, formatted_time = self._strptime_default_year(expression)
2770        function_name = "STRPTIME" if not expression.args.get("safe") else "TRY_STRPTIME"
2771        return self.sql(
2772            exp.cast(
2773                self.func(function_name, value, formatted_time),
2774                exp.DataType(this=exp.DType.DATE),
2775            )
2776        )
def parsedatetime_sql(self, expression: sqlglot.expressions.temporal.ParseDatetime) -> str:
2790    def parsedatetime_sql(self, expression: exp.ParseDatetime) -> str:
2791        value, formatted_time = self._strptime_default_year(expression)
2792        return self.func("STRPTIME", value, formatted_time)
def parsetime_sql(self, expression: sqlglot.expressions.temporal.ParseTime) -> str:
2794    def parsetime_sql(self, expression: exp.ParseTime) -> str:
2795        formatted_time = self.format_time(expression)
2796        return self.sql(
2797            exp.cast(
2798                self.func("STRPTIME", expression.this, formatted_time),
2799                exp.DataType(this=exp.DType.TIME),
2800            )
2801        )
def tsordstotime_sql(self, expression: sqlglot.expressions.temporal.TsOrDsToTime) -> str:
2803    def tsordstotime_sql(self, expression: exp.TsOrDsToTime) -> str:
2804        this = expression.this
2805        time_format = self.format_time(expression)
2806        safe = expression.args.get("safe")
2807        time_type = exp.DataType.from_str("TIME", dialect="duckdb")
2808        cast_expr = exp.TryCast if safe else exp.Cast
2809
2810        if time_format:
2811            func_name = "TRY_STRPTIME" if safe else "STRPTIME"
2812            strptime = exp.Anonymous(this=func_name, expressions=[this, time_format])
2813            return self.sql(cast_expr(this=strptime, to=time_type))
2814
2815        if isinstance(this, exp.TsOrDsToTime) or this.is_type(exp.DType.TIME):
2816            return self.sql(this)
2817
2818        return self.sql(cast_expr(this=this, to=time_type))
def currentdate_sql(self, expression: sqlglot.expressions.temporal.CurrentDate) -> str:
2820    def currentdate_sql(self, expression: exp.CurrentDate) -> str:
2821        if not expression.this:
2822            return "CURRENT_DATE"
2823
2824        expr = exp.Cast(
2825            this=exp.AtTimeZone(this=exp.CurrentTimestamp(), zone=expression.this),
2826            to=exp.DataType(this=exp.DType.DATE),
2827        )
2828        return self.sql(expr)
def checkjson_sql(self, expression: sqlglot.expressions.json.CheckJson) -> str:
2830    def checkjson_sql(self, expression: exp.CheckJson) -> str:
2831        arg = expression.this
2832        return self.sql(
2833            exp.case()
2834            .when(
2835                exp.or_(arg.is_(exp.Null()), arg.eq(""), exp.func("json_valid", arg)),
2836                exp.null(),
2837            )
2838            .else_(exp.Literal.string("Invalid JSON"))
2839        )
def parsejson_sql(self, expression: sqlglot.expressions.json.ParseJSON) -> str:
2841    def parsejson_sql(self, expression: exp.ParseJSON) -> str:
2842        arg = expression.this
2843        if expression.args.get("safe"):
2844            return self.sql(
2845                exp.case()
2846                .when(exp.func("json_valid", arg), exp.cast(arg.copy(), "JSON"))
2847                .else_(exp.null())
2848            )
2849        return self.func("JSON", arg)
def unicode_sql(self, expression: sqlglot.expressions.string.Unicode) -> str:
2851    def unicode_sql(self, expression: exp.Unicode) -> str:
2852        if expression.args.get("empty_is_zero"):
2853            return self.sql(
2854                exp.case()
2855                .when(expression.this.eq(exp.Literal.string("")), exp.Literal.number(0))
2856                .else_(exp.Anonymous(this="UNICODE", expressions=[expression.this]))
2857            )
2858
2859        return self.func("UNICODE", expression.this)
def stripnullvalue_sql(self, expression: sqlglot.expressions.json.StripNullValue) -> str:
2861    def stripnullvalue_sql(self, expression: exp.StripNullValue) -> str:
2862        return self.sql(
2863            exp.case()
2864            .when(exp.func("json_type", expression.this).eq("NULL"), exp.null())
2865            .else_(expression.this)
2866        )
def trunc_sql(self, expression: sqlglot.expressions.math.Trunc) -> str:
2868    def trunc_sql(self, expression: exp.Trunc) -> str:
2869        decimals = expression.args.get("decimals")
2870        if (
2871            expression.args.get("fractions_supported")
2872            and decimals
2873            and not decimals.is_type(exp.DType.INT)
2874        ):
2875            decimals = exp.cast(decimals, exp.DType.INT, dialect="duckdb")
2876
2877        return self.func("TRUNC", expression.this, decimals)
def normal_sql(self, expression: sqlglot.expressions.functions.Normal) -> str:
2879    def normal_sql(self, expression: exp.Normal) -> str:
2880        """
2881        Transpile Snowflake's NORMAL(mean, stddev, gen) to DuckDB.
2882
2883        Uses the Box-Muller transform via NORMAL_TEMPLATE.
2884        """
2885        mean = expression.this
2886        stddev = expression.args["stddev"]
2887        gen: exp.Expr = expression.args["gen"]
2888
2889        # Build two uniform random values [0, 1) for Box-Muller transform
2890        if isinstance(gen, exp.Rand) and gen.this is None:
2891            u1: exp.Expr = exp.Rand()
2892            u2: exp.Expr = exp.Rand()
2893        else:
2894            # Seeded: derive two values using HASH with different inputs
2895            seed = gen.this if isinstance(gen, exp.Rand) else gen
2896            u1 = exp.replace_placeholders(self.SEEDED_RANDOM_TEMPLATE, seed=seed)
2897            u2 = exp.replace_placeholders(
2898                self.SEEDED_RANDOM_TEMPLATE,
2899                seed=exp.Add(this=seed.copy(), expression=exp.Literal.number(1)),
2900            )
2901
2902        replacements = {"mean": mean, "stddev": stddev, "u1": u1, "u2": u2}
2903        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:
2905    def uniform_sql(self, expression: exp.Uniform) -> str:
2906        """
2907        Transpile Snowflake's UNIFORM(min, max, gen) to DuckDB.
2908
2909        UNIFORM returns a random value in [min, max]:
2910        - Integer result if both min and max are integers
2911        - Float result if either min or max is a float
2912        """
2913        min_val = expression.this
2914        max_val = expression.expression
2915        gen = expression.args.get("gen")
2916
2917        # Determine if result should be integer (both bounds are integers).
2918        # We do this to emulate Snowflake's behavior, INT -> INT, FLOAT -> FLOAT
2919        is_int_result = min_val.is_int and max_val.is_int
2920
2921        # Build the random value expression [0, 1)
2922        if not isinstance(gen, exp.Rand):
2923            # Seed value: (ABS(HASH(seed)) % 1000000) / 1000000.0
2924            random_expr: exp.Expr = exp.Div(
2925                this=exp.Paren(
2926                    this=exp.Mod(
2927                        this=exp.Abs(this=exp.Anonymous(this="HASH", expressions=[gen])),
2928                        expression=exp.Literal.number(1000000),
2929                    )
2930                ),
2931                expression=exp.Literal.number(1000000.0),
2932            )
2933        else:
2934            random_expr = exp.Rand()
2935
2936        # Build: min + random * (max - min [+ 1 for int])
2937        range_expr: exp.Expr = exp.Sub(this=max_val, expression=min_val)
2938        if is_int_result:
2939            range_expr = exp.Add(this=range_expr, expression=exp.Literal.number(1))
2940
2941        result: exp.Expr = exp.Add(
2942            this=min_val,
2943            expression=exp.Mul(this=random_expr, expression=exp.Paren(this=range_expr)),
2944        )
2945
2946        if is_int_result:
2947            result = exp.Cast(this=exp.Floor(this=result), to=exp.DType.BIGINT.into_expr())
2948
2949        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:
2951    def timefromparts_sql(self, expression: exp.TimeFromParts) -> str:
2952        nano = expression.args.get("nano")
2953        overflow = expression.args.get("overflow")
2954
2955        # Snowflake's TIME_FROM_PARTS supports overflow
2956        if overflow:
2957            hour = expression.args["hour"]
2958            minute = expression.args["min"]
2959            sec = expression.args["sec"]
2960
2961            # Check if values are within normal ranges - use MAKE_TIME for efficiency
2962            if not nano and all(arg.is_int for arg in [hour, minute, sec]):
2963                try:
2964                    h_val = hour.to_py()
2965                    m_val = minute.to_py()
2966                    s_val = sec.to_py()
2967                    if 0 <= h_val <= 23 and 0 <= m_val <= 59 and 0 <= s_val <= 59:
2968                        return rename_func("MAKE_TIME")(self, expression)
2969                except ValueError:
2970                    pass
2971
2972            # Overflow or nanoseconds detected - use INTERVAL arithmetic
2973            if nano:
2974                sec = sec + nano.pop() / exp.Literal.number(1000000000.0)
2975
2976            total_seconds = hour * exp.Literal.number(3600) + minute * exp.Literal.number(60) + sec
2977
2978            return self.sql(
2979                exp.Add(
2980                    this=exp.Cast(
2981                        this=exp.Literal.string("00:00:00"), to=exp.DType.TIME.into_expr()
2982                    ),
2983                    expression=exp.Interval(this=total_seconds, unit=exp.var("SECOND")),
2984                )
2985            )
2986
2987        # Default: MAKE_TIME
2988        if nano:
2989            expression.set(
2990                "sec", expression.args["sec"] + nano.pop() / exp.Literal.number(1000000000.0)
2991            )
2992
2993        return rename_func("MAKE_TIME")(self, expression)
def extract_sql(self, expression: sqlglot.expressions.temporal.Extract) -> str:
2995    def extract_sql(self, expression: exp.Extract) -> str:
2996        """
2997        Transpile EXTRACT/DATE_PART for DuckDB, handling specifiers not natively supported.
2998
2999        DuckDB doesn't support: WEEKISO, YEAROFWEEK, YEAROFWEEKISO, NANOSECOND,
3000        EPOCH_SECOND (as integer), EPOCH_MILLISECOND, EPOCH_MICROSECOND, EPOCH_NANOSECOND
3001        """
3002        this = expression.this
3003        datetime_expr = expression.expression
3004
3005        # TIMESTAMPTZ extractions may produce different results between Snowflake and DuckDB
3006        # because Snowflake applies server timezone while DuckDB uses local timezone
3007        if datetime_expr.is_type(exp.DType.TIMESTAMPTZ, exp.DType.TIMESTAMPLTZ):
3008            self.unsupported(
3009                "EXTRACT from TIMESTAMPTZ / TIMESTAMPLTZ may produce different results due to timezone handling differences"
3010            )
3011
3012        part_name = this.name.upper()
3013
3014        if part_name in self.EXTRACT_STRFTIME_MAPPINGS:
3015            fmt, cast_type = self.EXTRACT_STRFTIME_MAPPINGS[part_name]
3016
3017            # Problem: strftime doesn't accept TIME and there's no NANOSECOND function
3018            # So, for NANOSECOND with TIME, fallback to MICROSECOND * 1000
3019            is_nano_time = part_name == "NANOSECOND" and datetime_expr.is_type(
3020                exp.DType.TIME, exp.DType.TIMETZ
3021            )
3022
3023            if is_nano_time:
3024                self.unsupported("Parameter NANOSECOND is not supported with TIME type in DuckDB")
3025                return self.sql(
3026                    exp.cast(
3027                        exp.Mul(
3028                            this=exp.Extract(this=exp.var("MICROSECOND"), expression=datetime_expr),
3029                            expression=exp.Literal.number(1000),
3030                        ),
3031                        exp.DataType.from_str(cast_type, dialect="duckdb"),
3032                    )
3033                )
3034
3035            # For NANOSECOND, cast to TIMESTAMP_NS to preserve nanosecond precision
3036            strftime_input = datetime_expr
3037            if part_name == "NANOSECOND":
3038                strftime_input = exp.cast(datetime_expr, exp.DType.TIMESTAMP_NS)
3039
3040            return self.sql(
3041                exp.cast(
3042                    exp.Anonymous(
3043                        this="STRFTIME",
3044                        expressions=[strftime_input, exp.Literal.string(fmt)],
3045                    ),
3046                    exp.DataType.from_str(cast_type, dialect="duckdb"),
3047                )
3048            )
3049
3050        if part_name in self.EXTRACT_EPOCH_MAPPINGS:
3051            func_name = self.EXTRACT_EPOCH_MAPPINGS[part_name]
3052            result: exp.Expr = exp.Anonymous(this=func_name, expressions=[datetime_expr])
3053            # EPOCH returns float, cast to BIGINT for integer result
3054            if part_name == "EPOCH_SECOND":
3055                result = exp.cast(result, exp.DataType.from_str("BIGINT", dialect="duckdb"))
3056            return self.sql(result)
3057
3058        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:
3060    def timestampfromparts_sql(self, expression: exp.TimestampFromParts) -> str:
3061        # Check if this is the date/time expression form: TIMESTAMP_FROM_PARTS(date_expr, time_expr)
3062        date_expr = expression.this
3063        time_expr = expression.expression
3064
3065        if date_expr is not None and time_expr is not None:
3066            # In DuckDB, DATE + TIME produces TIMESTAMP
3067            return self.sql(exp.Add(this=date_expr, expression=time_expr))
3068
3069        # Component-based form: TIMESTAMP_FROM_PARTS(year, month, day, hour, minute, second, ...)
3070        sec = expression.args.get("sec")
3071        if sec is None:
3072            # This shouldn't happen with valid input, but handle gracefully
3073            return rename_func("MAKE_TIMESTAMP")(self, expression)
3074
3075        milli = expression.args.get("milli")
3076        if milli is not None:
3077            sec += milli.pop() / exp.Literal.number(1000.0)
3078
3079        nano = expression.args.get("nano")
3080        if nano is not None:
3081            sec += nano.pop() / exp.Literal.number(1000000000.0)
3082
3083        if milli or nano:
3084            expression.set("sec", sec)
3085
3086        return rename_func("MAKE_TIMESTAMP")(self, expression)
@unsupported_args('nano')
def timestampltzfromparts_sql( self, expression: sqlglot.expressions.temporal.TimestampLtzFromParts) -> str:
3088    @unsupported_args("nano")
3089    def timestampltzfromparts_sql(self, expression: exp.TimestampLtzFromParts) -> str:
3090        # Pop nano so rename_func only passes args that MAKE_TIMESTAMP accepts
3091        if nano := expression.args.get("nano"):
3092            nano.pop()
3093
3094        timestamp = rename_func("MAKE_TIMESTAMP")(self, expression)
3095        return f"CAST({timestamp} AS TIMESTAMPTZ)"
@unsupported_args('nano')
def timestamptzfromparts_sql( self, expression: sqlglot.expressions.temporal.TimestampTzFromParts) -> str:
3097    @unsupported_args("nano")
3098    def timestamptzfromparts_sql(self, expression: exp.TimestampTzFromParts) -> str:
3099        # Extract zone before popping
3100        zone = expression.args.get("zone")
3101        # Pop zone and nano so rename_func only passes args that MAKE_TIMESTAMP accepts
3102        if zone:
3103            zone = zone.pop()
3104
3105        if nano := expression.args.get("nano"):
3106            nano.pop()
3107
3108        timestamp = rename_func("MAKE_TIMESTAMP")(self, expression)
3109
3110        if zone:
3111            # Use AT TIME ZONE to apply the explicit timezone
3112            return f"{timestamp} AT TIME ZONE {self.sql(zone)}"
3113
3114        return timestamp
def tablesample_sql( self, expression: sqlglot.expressions.query.TableSample, tablesample_keyword: str | None = None) -> str:
3116    def tablesample_sql(
3117        self,
3118        expression: exp.TableSample,
3119        tablesample_keyword: str | None = None,
3120    ) -> str:
3121        if not isinstance(expression.parent, exp.Select):
3122            # This sample clause only applies to a single source, not the entire resulting relation
3123            tablesample_keyword = "TABLESAMPLE"
3124
3125        if expression.args.get("size"):
3126            method = expression.args.get("method")
3127            if method and method.name.upper() != "RESERVOIR":
3128                self.unsupported(
3129                    f"Sampling method {method} is not supported with a discrete sample count, "
3130                    "defaulting to reservoir sampling"
3131                )
3132                expression.set("method", exp.var("RESERVOIR"))
3133
3134        return super().tablesample_sql(expression, tablesample_keyword=tablesample_keyword)
def in_sql(self, expression: sqlglot.expressions.core.In) -> str:
3136    def in_sql(self, expression: exp.In) -> str:
3137        unnest = expression.args.get("unnest")
3138        if unnest:
3139            return self.sql(
3140                exp.replace_placeholders(
3141                    self.IN_UNNEST_TEMPLATE, arr=unnest.expressions[0], value=expression.this
3142                )
3143            )
3144        return super().in_sql(expression)
def join_sql(self, expression: sqlglot.expressions.query.Join) -> str:
3146    def join_sql(self, expression: exp.Join) -> str:
3147        if (
3148            not expression.args.get("using")
3149            and not expression.args.get("on")
3150            and not expression.method
3151            and (expression.kind in ("", "INNER", "OUTER"))
3152        ):
3153            # Some dialects support `LEFT/INNER JOIN UNNEST(...)` without an explicit ON clause
3154            # DuckDB doesn't, but we can just add a dummy ON clause that is always true
3155            if isinstance(expression.this, exp.Unnest):
3156                return super().join_sql(expression.on(exp.true()))
3157
3158            expression.set("side", None)
3159            expression.set("kind", None)
3160
3161        return super().join_sql(expression)
def countif_sql(self, expression: sqlglot.expressions.aggregate.CountIf) -> str:
3163    def countif_sql(self, expression: exp.CountIf) -> str:
3164        if self.dialect.version >= (1, 2):
3165            this = expression.this
3166            if expression.args.get("zero_on_all_null") and not isinstance(this, exp.Distinct):
3167                # DuckDB >= 1.2's COUNT_IF returns NULL when the condition is NULL on all rows,
3168                # so we wrap the condition in IS TRUE to preserve count-like semantics
3169                expression = exp.CountIf(this=exp.paren(this).is_(exp.true()))
3170            return self.function_fallback_sql(expression)
3171
3172        # https://github.com/tobymao/sqlglot/pull/4749
3173        return count_if_to_sum(self, expression)
def bracket_sql(self, expression: sqlglot.expressions.core.Bracket) -> str:
3175    def bracket_sql(self, expression: exp.Bracket) -> str:
3176        if self.dialect.version >= (1, 2):
3177            return super().bracket_sql(expression)
3178
3179        # https://duckdb.org/2025/02/05/announcing-duckdb-120.html#breaking-changes
3180        this = expression.this
3181        if isinstance(this, exp.Array):
3182            this.replace(exp.paren(this))
3183
3184        bracket = super().bracket_sql(expression)
3185
3186        if not expression.args.get("returns_list_for_maps"):
3187            if not this.type:
3188                from sqlglot.optimizer.annotate_types import annotate_types
3189
3190                this = annotate_types(this, dialect=self.dialect)
3191
3192            if this.is_type(exp.DType.MAP):
3193                bracket = f"({bracket})[1]"
3194
3195        return bracket
def withingroup_sql(self, expression: sqlglot.expressions.core.WithinGroup) -> str:
3197    def withingroup_sql(self, expression: exp.WithinGroup) -> str:
3198        func = expression.this
3199
3200        # For ARRAY_AGG, DuckDB requires ORDER BY inside the function, not in WITHIN GROUP
3201        # Transform: ARRAY_AGG(x) WITHIN GROUP (ORDER BY y) -> ARRAY_AGG(x ORDER BY y)
3202        if isinstance(func, exp.ArrayAgg):
3203            if not isinstance(order := expression.expression, exp.Order):
3204                return self.sql(func)
3205
3206            # Save the original column for FILTER clause (before wrapping with Order)
3207            original_this = func.this
3208
3209            # Move ORDER BY inside ARRAY_AGG by wrapping its argument with Order
3210            # ArrayAgg.this should become Order(this=ArrayAgg.this, expressions=order.expressions)
3211            func.set(
3212                "this",
3213                exp.Order(
3214                    this=func.this.copy(),
3215                    expressions=order.expressions,
3216                ),
3217            )
3218
3219            # Generate the ARRAY_AGG function with ORDER BY and add FILTER clause if needed
3220            # Use original_this (not the Order-wrapped version) for the FILTER condition
3221            array_agg_sql = self.function_fallback_sql(func)
3222            return self._add_arrayagg_null_filter(array_agg_sql, func, original_this)
3223
3224        # For other functions (like PERCENTILES), use existing logic
3225        expression_sql = self.sql(expression, "expression")
3226
3227        if isinstance(func, exp.PERCENTILES):
3228            # Make the order key the first arg and slide the fraction to the right
3229            # https://duckdb.org/docs/sql/aggregates#ordered-set-aggregate-functions
3230            order_col = expression.find(exp.Ordered)
3231            if order_col:
3232                func.set("expression", func.this)
3233                func.set("this", order_col.this)
3234
3235        this = self.sql(expression, "this").rstrip(")")
3236
3237        return f"{this}{expression_sql})"
def length_sql(self, expression: sqlglot.expressions.string.Length) -> str:
3239    def length_sql(self, expression: exp.Length) -> str:
3240        arg = expression.this
3241
3242        # Dialects like BQ and Snowflake also accept binary values as args, so
3243        # DDB will attempt to infer the type or resort to case/when resolution
3244        if not expression.args.get("binary") or arg.is_string:
3245            return self.func("LENGTH", arg)
3246
3247        if not arg.type:
3248            from sqlglot.optimizer.annotate_types import annotate_types
3249
3250            arg = annotate_types(arg, dialect=self.dialect)
3251
3252        if arg.is_type(*exp.DataType.TEXT_TYPES):
3253            return self.func("LENGTH", arg)
3254
3255        # We need these casts to make duckdb's static type checker happy
3256        blob = exp.cast(arg, exp.DType.VARBINARY)
3257        varchar = exp.cast(arg, exp.DType.VARCHAR)
3258
3259        case = (
3260            exp.case(exp.Anonymous(this="TYPEOF", expressions=[arg]))
3261            .when(exp.Literal.string("BLOB"), exp.ByteLength(this=blob))
3262            .else_(exp.Anonymous(this="LENGTH", expressions=[varchar]))
3263        )
3264        return self.sql(case)
def bitlength_sql(self, expression: sqlglot.expressions.string.BitLength) -> str:
3266    def bitlength_sql(self, expression: exp.BitLength) -> str:
3267        if not _is_binary(arg := expression.this):
3268            return self.func("BIT_LENGTH", arg)
3269
3270        blob = exp.cast(arg, exp.DataType.Type.VARBINARY)
3271        return self.sql(exp.ByteLength(this=blob) * exp.Literal.number(8))
def chr_sql( self, expression: sqlglot.expressions.string.Chr, name: str = 'CHR') -> str:
3273    def chr_sql(self, expression: exp.Chr, name: str = "CHR") -> str:
3274        arg = expression.expressions[0]
3275        if arg.is_type(*exp.DataType.REAL_TYPES):
3276            arg = exp.cast(arg, exp.DType.INT)
3277        return self.func("CHR", arg)
def collation_sql(self, expression: sqlglot.expressions.functions.Collation) -> str:
3279    def collation_sql(self, expression: exp.Collation) -> str:
3280        self.unsupported("COLLATION function is not supported by DuckDB")
3281        return self.function_fallback_sql(expression)
def collate_sql(self, expression: sqlglot.expressions.functions.Collate) -> str:
3283    def collate_sql(self, expression: exp.Collate) -> str:
3284        if not expression.expression.is_string:
3285            return super().collate_sql(expression)
3286
3287        raw = expression.expression.name
3288        if not raw:
3289            return self.sql(expression.this)
3290
3291        parts = []
3292        for part in raw.split("-"):
3293            lower = part.lower()
3294            if lower not in _SNOWFLAKE_COLLATION_DEFAULTS:
3295                if lower in _SNOWFLAKE_COLLATION_UNSUPPORTED:
3296                    self.unsupported(
3297                        f"Snowflake collation specifier '{part}' has no DuckDB equivalent"
3298                    )
3299                parts.append(lower)
3300
3301        if not parts:
3302            return self.sql(expression.this)
3303        return super().collate_sql(
3304            exp.Collate(this=expression.this, expression=exp.var(".".join(parts)))
3305        )
def regexpcount_sql(self, expression: sqlglot.expressions.string.RegexpCount) -> str:
3337    def regexpcount_sql(self, expression: exp.RegexpCount) -> str:
3338        this = expression.this
3339        pattern = expression.expression
3340        position = expression.args.get("position")
3341        parameters = expression.args.get("parameters")
3342
3343        # Validate flags - only "ims" flags are supported for embedded patterns
3344        validated_flags = self._validate_regexp_flags(parameters, supported_flags="ims")
3345
3346        if position:
3347            this = exp.Substring(this=this, start=position)
3348
3349        # Embed flags in pattern (REGEXP_EXTRACT_ALL doesn't support flags argument)
3350        if validated_flags:
3351            pattern = exp.Concat(expressions=[exp.Literal.string(f"(?{validated_flags})"), pattern])
3352
3353        # Handle empty pattern: Snowflake returns 0, DuckDB would match between every character
3354        result = (
3355            exp.case()
3356            .when(
3357                exp.EQ(this=pattern, expression=exp.Literal.string("")),
3358                exp.Literal.number(0),
3359            )
3360            .else_(
3361                exp.Length(
3362                    this=exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern])
3363                )
3364            )
3365        )
3366
3367        return self.sql(result)
def regexpreplace_sql(self, expression: sqlglot.expressions.string.RegexpReplace) -> str:
3369    def regexpreplace_sql(self, expression: exp.RegexpReplace) -> str:
3370        subject = expression.this
3371        pattern = expression.expression
3372        replacement = expression.args.get("replacement") or exp.Literal.string("")
3373        position = expression.args.get("position")
3374        occurrence = expression.args.get("occurrence")
3375        modifiers = expression.args.get("modifiers")
3376
3377        validated_flags = self._validate_regexp_flags(modifiers, supported_flags="cimsg") or ""
3378
3379        # Handle occurrence (only literals supported)
3380        if occurrence and not occurrence.is_int:
3381            self.unsupported("REGEXP_REPLACE with non-literal occurrence")
3382        else:
3383            occurrence = occurrence.to_py() if occurrence and occurrence.is_int else 0
3384            if occurrence > 1:
3385                self.unsupported(f"REGEXP_REPLACE occurrence={occurrence} not supported")
3386            # flag duckdb to do either all or none, single_replace check is for duckdb round trip
3387            elif (
3388                occurrence == 0
3389                and "g" not in validated_flags
3390                and not expression.args.get("single_replace")
3391            ):
3392                validated_flags += "g"
3393
3394        # Handle position (only literals supported)
3395        prefix = None
3396        if position and not position.is_int:
3397            self.unsupported("REGEXP_REPLACE with non-literal position")
3398        elif position and position.is_int and position.to_py() > 1:
3399            pos = position.to_py()
3400            prefix = exp.Substring(
3401                this=subject, start=exp.Literal.number(1), length=exp.Literal.number(pos - 1)
3402            )
3403            subject = exp.Substring(this=subject, start=exp.Literal.number(pos))
3404
3405        result: exp.Expr = exp.Anonymous(
3406            this="REGEXP_REPLACE",
3407            expressions=[
3408                subject,
3409                pattern,
3410                replacement,
3411                exp.Literal.string(validated_flags) if validated_flags else None,
3412            ],
3413        )
3414
3415        if prefix:
3416            result = exp.Concat(expressions=[prefix, result])
3417
3418        return self.sql(result)
def regexplike_sql(self, expression: sqlglot.expressions.core.RegexpLike) -> str:
3420    def regexplike_sql(self, expression: exp.RegexpLike) -> str:
3421        this = expression.this
3422        pattern = expression.expression
3423        flag = expression.args.get("flag")
3424
3425        if expression.args.get("full_match"):
3426            validated_flags = self._validate_regexp_flags(flag, supported_flags="cims")
3427            flag = exp.Literal.string(validated_flags) if validated_flags else None
3428            return self.func("REGEXP_FULL_MATCH", this, pattern, flag)
3429
3430        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:
3432    @unsupported_args("ins_cost", "del_cost", "sub_cost")
3433    def levenshtein_sql(self, expression: exp.Levenshtein) -> str:
3434        this = expression.this
3435        expr = expression.expression
3436        max_dist = expression.args.get("max_dist")
3437
3438        if max_dist is None:
3439            return self.func("LEVENSHTEIN", this, expr)
3440
3441        # Emulate Snowflake semantics: if distance > max_dist, return max_dist
3442        levenshtein = exp.Levenshtein(this=this, expression=expr)
3443        return self.sql(exp.Least(this=levenshtein, expressions=[max_dist]))
def pad_sql(self, expression: sqlglot.expressions.string.Pad) -> str:
3445    def pad_sql(self, expression: exp.Pad) -> str:
3446        """
3447        Handle RPAD/LPAD for VARCHAR and BINARY types.
3448
3449        For VARCHAR: Delegate to parent class
3450        For BINARY: Lower to: input || REPEAT(pad, GREATEST(0, target_len - OCTET_LENGTH(input)))
3451        """
3452        string_arg = expression.this
3453        fill_arg = expression.args.get("fill_pattern") or exp.Literal.string(" ")
3454
3455        if _is_binary(string_arg) or _is_binary(fill_arg):
3456            length_arg = expression.expression
3457            is_left = expression.args.get("is_left")
3458
3459            input_len = exp.ByteLength(this=string_arg)
3460            chars_needed = length_arg - input_len
3461            pad_count = exp.Greatest(
3462                this=exp.Literal.number(0), expressions=[chars_needed], ignore_nulls=True
3463            )
3464            repeat_expr = exp.Repeat(this=fill_arg, times=pad_count)
3465
3466            left, right = string_arg, repeat_expr
3467            if is_left:
3468                left, right = right, left
3469
3470            result = exp.DPipe(this=left, expression=right)
3471            return self.sql(result)
3472
3473        # For VARCHAR: Delegate to parent class (handles PAD_FILL_PATTERN_IS_REQUIRED)
3474        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:
3476    def minhash_sql(self, expression: exp.Minhash) -> str:
3477        k = expression.this
3478        exprs = expression.expressions
3479
3480        if len(exprs) != 1 or isinstance(exprs[0], exp.Star):
3481            self.unsupported(
3482                "MINHASH with multiple expressions or * requires manual query restructuring"
3483            )
3484            return self.func("MINHASH", k, *exprs)
3485
3486        expr = exprs[0]
3487        result = exp.replace_placeholders(self.MINHASH_TEMPLATE.copy(), expr=expr, k=k)
3488        return f"({self.sql(result)})"
def minhashcombine_sql(self, expression: sqlglot.expressions.aggregate.MinhashCombine) -> str:
3490    def minhashcombine_sql(self, expression: exp.MinhashCombine) -> str:
3491        expr = expression.this
3492        result = exp.replace_placeholders(self.MINHASH_COMBINE_TEMPLATE.copy(), expr=expr)
3493        return f"({self.sql(result)})"
def approximatesimilarity_sql( self, expression: sqlglot.expressions.aggregate.ApproximateSimilarity) -> str:
3495    def approximatesimilarity_sql(self, expression: exp.ApproximateSimilarity) -> str:
3496        expr = expression.this
3497        result = exp.replace_placeholders(self.APPROXIMATE_SIMILARITY_TEMPLATE.copy(), expr=expr)
3498        return f"({self.sql(result)})"
def arrayuniqueagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayUniqueAgg) -> str:
3500    def arrayuniqueagg_sql(self, expression: exp.ArrayUniqueAgg) -> str:
3501        return self.sql(
3502            exp.Filter(
3503                this=exp.func("LIST", exp.Distinct(expressions=[expression.this])),
3504                expression=exp.Where(this=expression.this.copy().is_(exp.null()).not_()),
3505            )
3506        )
def arrayconcatagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayConcatAgg) -> str:
3508    def arrayconcatagg_sql(self, expression: exp.ArrayConcatAgg) -> str:
3509        this = expression.this
3510
3511        if isinstance(this, exp.Limit):
3512            self.unsupported("LIMIT in ARRAY_CONCAT_AGG cannot be transpiled to DuckDB")
3513            this = this.this
3514
3515        inner = this.this if isinstance(this, exp.Order) else this
3516
3517        return self.func(
3518            "FLATTEN",
3519            exp.Filter(
3520                this=exp.ArrayAgg(this=this),
3521                expression=exp.Where(this=inner.copy().is_(exp.null()).not_()),
3522            ),
3523        )
def arrayunionagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayUnionAgg) -> str:
3525    def arrayunionagg_sql(self, expression: exp.ArrayUnionAgg) -> str:
3526        self.unsupported("ARRAY_UNION_AGG is not supported in DuckDB")
3527        return self.function_fallback_sql(expression)
def arraydistinct_sql(self, expression: sqlglot.expressions.array.ArrayDistinct) -> str:
3529    def arraydistinct_sql(self, expression: exp.ArrayDistinct) -> str:
3530        arr = expression.this
3531        func = self.func("LIST_DISTINCT", arr)
3532
3533        if expression.args.get("check_null"):
3534            add_null_to_array = exp.func(
3535                "LIST_APPEND", exp.func("LIST_DISTINCT", exp.ArrayCompact(this=arr)), exp.Null()
3536            )
3537            return self.sql(
3538                exp.If(
3539                    this=exp.NEQ(
3540                        this=exp.ArraySize(this=arr), expression=exp.func("LIST_COUNT", arr)
3541                    ),
3542                    true=add_null_to_array,
3543                    false=func,
3544                )
3545            )
3546
3547        return func
def arrayintersect_sql(self, expression: sqlglot.expressions.array.ArrayIntersect) -> str:
3549    def arrayintersect_sql(self, expression: exp.ArrayIntersect) -> str:
3550        if expression.args.get("is_multiset") and len(expression.expressions) == 2:
3551            return self._array_bag_sql(
3552                self.ARRAY_INTERSECTION_CONDITION,
3553                expression.expressions[0],
3554                expression.expressions[1],
3555            )
3556        return self.function_fallback_sql(expression)
def arrayexcept_sql(self, expression: sqlglot.expressions.array.ArrayExcept) -> str:
3558    def arrayexcept_sql(self, expression: exp.ArrayExcept) -> str:
3559        arr1, arr2 = expression.this, expression.expression
3560        if expression.args.get("is_multiset"):
3561            return self._array_bag_sql(self.ARRAY_EXCEPT_CONDITION, arr1, arr2)
3562        return self.sql(
3563            exp.replace_placeholders(self.ARRAY_EXCEPT_SET_TEMPLATE, arr1=arr1, arr2=arr2)
3564        )
def arrayslice_sql(self, expression: sqlglot.expressions.array.ArraySlice) -> str:
3566    def arrayslice_sql(self, expression: exp.ArraySlice) -> str:
3567        """
3568        Transpiles Snowflake's ARRAY_SLICE (0-indexed, exclusive end) to DuckDB's
3569        ARRAY_SLICE (1-indexed, inclusive end) by wrapping start and end in CASE
3570        expressions that adjust the index at query time:
3571          - start: CASE WHEN start >= 0 THEN start + 1 ELSE start END
3572          - end:   CASE WHEN end < 0 THEN end - 1 ELSE end END
3573        """
3574        start, end = expression.args.get("start"), expression.args.get("end")
3575
3576        if expression.args.get("zero_based"):
3577            if start is not None:
3578                start = (
3579                    exp.case()
3580                    .when(
3581                        exp.GTE(this=start.copy(), expression=exp.Literal.number(0)),
3582                        exp.Add(this=start.copy(), expression=exp.Literal.number(1)),
3583                    )
3584                    .else_(start)
3585                )
3586            if end is not None:
3587                end = (
3588                    exp.case()
3589                    .when(
3590                        exp.LT(this=end.copy(), expression=exp.Literal.number(0)),
3591                        exp.Sub(this=end.copy(), expression=exp.Literal.number(1)),
3592                    )
3593                    .else_(end)
3594                )
3595
3596        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:
3598    def arrayszip_sql(self, expression: exp.ArraysZip) -> str:
3599        args = expression.expressions
3600
3601        if not args:
3602            # Return [{}] - using MAP([], []) since DuckDB can't represent empty structs
3603            return self.sql(exp.array(exp.Map(keys=exp.array(), values=exp.array())))
3604
3605        # Build placeholder values for template
3606        lengths = [exp.Length(this=arg) for arg in args]
3607        max_len = (
3608            lengths[0]
3609            if len(lengths) == 1
3610            else exp.Greatest(this=lengths[0], expressions=lengths[1:])
3611        )
3612
3613        # Empty struct with same schema: {'$1': NULL, '$2': NULL, ...}
3614        empty_struct = exp.func(
3615            "STRUCT",
3616            *[
3617                exp.PropertyEQ(this=exp.Literal.string(f"${i + 1}"), expression=exp.Null())
3618                for i in range(len(args))
3619            ],
3620        )
3621
3622        # Struct for transform: {'$1': COALESCE(arr1, [])[__i + 1], ...}
3623        # COALESCE wrapping handles NULL arrays - prevents invalid NULL[i] syntax
3624        index = exp.column("__i") + 1
3625        transform_struct = exp.func(
3626            "STRUCT",
3627            *[
3628                exp.PropertyEQ(
3629                    this=exp.Literal.string(f"${i + 1}"),
3630                    expression=exp.func("COALESCE", arg, exp.array())[index],
3631                )
3632                for i, arg in enumerate(args)
3633            ],
3634        )
3635
3636        result = exp.replace_placeholders(
3637            self.ARRAYS_ZIP_TEMPLATE.copy(),
3638            null_check=exp.or_(*[arg.is_(exp.Null()) for arg in args]),
3639            all_empty_check=exp.and_(
3640                *[
3641                    exp.EQ(this=exp.Length(this=arg), expression=exp.Literal.number(0))
3642                    for arg in args
3643                ]
3644            ),
3645            empty_struct=empty_struct,
3646            max_len=max_len,
3647            transform_struct=transform_struct,
3648        )
3649        return self.sql(result)
def lower_sql(self, expression: sqlglot.expressions.string.Lower) -> str:
3651    def lower_sql(self, expression: exp.Lower) -> str:
3652        result_sql = self.func("LOWER", _cast_to_varchar(expression.this))
3653        return _gen_with_cast_to_blob(self, expression, result_sql)
def upper_sql(self, expression: sqlglot.expressions.string.Upper) -> str:
3655    def upper_sql(self, expression: exp.Upper) -> str:
3656        result_sql = self.func("UPPER", _cast_to_varchar(expression.this))
3657        return _gen_with_cast_to_blob(self, expression, result_sql)
def reverse_sql(self, expression: sqlglot.expressions.string.Reverse) -> str:
3659    def reverse_sql(self, expression: exp.Reverse) -> str:
3660        result_sql = self.func("REVERSE", _cast_to_varchar(expression.this))
3661        return _gen_with_cast_to_blob(self, expression, result_sql)
def left_sql(self, expression: sqlglot.expressions.string.Left) -> str:
3687    def left_sql(self, expression: exp.Left) -> str:
3688        return self._left_right_sql(expression, "LEFT")
def right_sql(self, expression: sqlglot.expressions.string.Right) -> str:
3690    def right_sql(self, expression: exp.Right) -> str:
3691        return self._left_right_sql(expression, "RIGHT")
def rtrimmedlength_sql(self, expression: sqlglot.expressions.string.RtrimmedLength) -> str:
3693    def rtrimmedlength_sql(self, expression: exp.RtrimmedLength) -> str:
3694        return self.func("LENGTH", exp.Trim(this=expression.this, position="TRAILING"))
def stuff_sql(self, expression: sqlglot.expressions.string.Stuff) -> str:
3696    def stuff_sql(self, expression: exp.Stuff) -> str:
3697        base = expression.this
3698        start = expression.args["start"]
3699        length = expression.args["length"]
3700        insertion = expression.expression
3701        is_binary = _is_binary(base)
3702
3703        if is_binary:
3704            # DuckDB's SUBSTRING doesn't accept BLOB; operate on the HEX string instead
3705            # (each byte = 2 hex chars), then UNHEX back to BLOB
3706            base = exp.Hex(this=base)
3707            insertion = exp.Hex(this=insertion)
3708            left = exp.Substring(
3709                this=base.copy(),
3710                start=exp.Literal.number(1),
3711                length=(start.copy() - exp.Literal.number(1)) * exp.Literal.number(2),
3712            )
3713            right = exp.Substring(
3714                this=base.copy(),
3715                start=((start + length) - exp.Literal.number(1)) * exp.Literal.number(2)
3716                + exp.Literal.number(1),
3717            )
3718        else:
3719            left = exp.Substring(
3720                this=base.copy(),
3721                start=exp.Literal.number(1),
3722                length=start.copy() - exp.Literal.number(1),
3723            )
3724            right = exp.Substring(this=base.copy(), start=start + length)
3725        result: exp.Expr = exp.DPipe(
3726            this=exp.DPipe(this=left, expression=insertion), expression=right
3727        )
3728
3729        if is_binary:
3730            result = exp.Unhex(this=result)
3731
3732        return self.sql(result)
def rand_sql(self, expression: sqlglot.expressions.functions.Rand) -> str:
3734    def rand_sql(self, expression: exp.Rand) -> str:
3735        seed = expression.this
3736        if seed is not None:
3737            self.unsupported("RANDOM with seed is not supported in DuckDB")
3738
3739        lower = expression.args.get("lower")
3740        upper = expression.args.get("upper")
3741
3742        if lower and upper:
3743            # scale DuckDB's [0,1) to the specified range
3744            range_size = exp.paren(upper - lower)
3745            scaled = exp.Add(this=lower, expression=exp.func("random") * range_size)
3746
3747            # For now we assume that if bounds are set, return type is BIGINT. Snowflake/Teradata
3748            result = exp.cast(scaled, exp.DType.BIGINT)
3749            return self.sql(result)
3750
3751        # Default DuckDB behavior - just return RANDOM() as float
3752        return "RANDOM()"
def bytelength_sql(self, expression: sqlglot.expressions.string.ByteLength) -> str:
3754    def bytelength_sql(self, expression: exp.ByteLength) -> str:
3755        arg = expression.this
3756
3757        # Check if it's a text type (handles both literals and annotated expressions)
3758        if arg.is_type(*exp.DataType.TEXT_TYPES):
3759            return self.func("OCTET_LENGTH", exp.Encode(this=arg))
3760
3761        # Default: pass through as-is (conservative for DuckDB, handles binary and unannotated)
3762        return self.func("OCTET_LENGTH", arg)
def base64encode_sql(self, expression: sqlglot.expressions.string.Base64Encode) -> str:
3764    def base64encode_sql(self, expression: exp.Base64Encode) -> str:
3765        # DuckDB TO_BASE64 requires BLOB input
3766        # Snowflake BASE64_ENCODE accepts both VARCHAR and BINARY - for VARCHAR it implicitly
3767        # encodes UTF-8 bytes. We add ENCODE unless the input is a binary type.
3768        result = expression.this
3769
3770        # Check if input is a string type - ENCODE only accepts VARCHAR
3771        if result.is_type(*exp.DataType.TEXT_TYPES):
3772            result = exp.Encode(this=result)
3773
3774        result = exp.ToBase64(this=result)
3775
3776        max_line_length = expression.args.get("max_line_length")
3777        alphabet = expression.args.get("alphabet")
3778
3779        # Handle custom alphabet by replacing standard chars with custom ones
3780        result = _apply_base64_alphabet_replacements(result, alphabet)
3781
3782        # Handle max_line_length by inserting newlines every N characters
3783        line_length = (
3784            t.cast(int, max_line_length.to_py())
3785            if isinstance(max_line_length, exp.Literal) and max_line_length.is_number
3786            else 0
3787        )
3788        if line_length > 0:
3789            newline = exp.Chr(expressions=[exp.Literal.number(10)])
3790            result = exp.Trim(
3791                this=exp.RegexpReplace(
3792                    this=result,
3793                    expression=exp.Literal.string(f"(.{{{line_length}}})"),
3794                    replacement=exp.Concat(expressions=[exp.Literal.string("\\1"), newline.copy()]),
3795                ),
3796                expression=newline,
3797                position="TRAILING",
3798            )
3799
3800        return self.sql(result)
def hex_sql(self, expression: sqlglot.expressions.string.Hex) -> str:
3802    def hex_sql(self, expression: exp.Hex) -> str:
3803        case = expression.args.get("case")
3804
3805        if not case:
3806            return self.func("HEX", expression.this)
3807
3808        hex_expr = exp.Hex(this=expression.this)
3809        return self.sql(
3810            exp.case()
3811            .when(case.is_(exp.null()), exp.null())
3812            .when(case.copy().eq(0), exp.Lower(this=hex_expr.copy()))
3813            .else_(hex_expr)
3814        )
def replace_sql(self, expression: sqlglot.expressions.string.Replace) -> str:
3816    def replace_sql(self, expression: exp.Replace) -> str:
3817        result_sql = self.func(
3818            "REPLACE",
3819            _cast_to_varchar(expression.this),
3820            _cast_to_varchar(expression.expression),
3821            _cast_to_varchar(expression.args.get("replacement")),
3822        )
3823        return _gen_with_cast_to_blob(self, expression, result_sql)
def bitwisexor_sql(self, expression: sqlglot.expressions.core.BitwiseXor) -> str:
3830    def bitwisexor_sql(self, expression: exp.BitwiseXor) -> str:
3831        _prepare_binary_bitwise_args(expression)
3832        result_sql = self.func("XOR", expression.this, expression.expression)
3833        return _gen_with_cast_to_blob(self, expression, result_sql)
def objectinsert_sql(self, expression: sqlglot.expressions.json.ObjectInsert) -> str:
3835    def objectinsert_sql(self, expression: exp.ObjectInsert) -> str:
3836        this = expression.this
3837        key = expression.args.get("key")
3838        key_sql = key.name if isinstance(key, exp.Expr) else ""
3839        value_sql = self.sql(expression, "value")
3840
3841        kv_sql = f"{key_sql} := {value_sql}"
3842
3843        # If the input struct is empty e.g. transpiling OBJECT_INSERT(OBJECT_CONSTRUCT(), key, value) from Snowflake
3844        # then we can generate STRUCT_PACK which will build it since STRUCT_INSERT({}, key := value) is not valid DuckDB
3845        if isinstance(this, exp.Struct) and not this.expressions:
3846            return self.func("STRUCT_PACK", kv_sql)
3847
3848        return self.func("STRUCT_INSERT", this, kv_sql)
def mapcat_sql(self, expression: sqlglot.expressions.array.MapCat) -> str:
3850    def mapcat_sql(self, expression: exp.MapCat) -> str:
3851        result = exp.replace_placeholders(
3852            self.MAPCAT_TEMPLATE.copy(),
3853            map1=expression.this,
3854            map2=expression.expression,
3855        )
3856        return self.sql(result)
def mapcontainskey_sql(self, expression: sqlglot.expressions.array.MapContainsKey) -> str:
3858    def mapcontainskey_sql(self, expression: exp.MapContainsKey) -> str:
3859        return self.func(
3860            "ARRAY_CONTAINS", exp.func("MAP_KEYS", expression.args["key"]), expression.this
3861        )
def mapdelete_sql(self, expression: sqlglot.expressions.array.MapDelete) -> str:
3863    def mapdelete_sql(self, expression: exp.MapDelete) -> str:
3864        map_arg = expression.this
3865        keys_to_delete = expression.expressions
3866
3867        x_dot_key = exp.Dot(this=exp.to_identifier("x"), expression=exp.to_identifier("key"))
3868
3869        lambda_expr = exp.Lambda(
3870            this=exp.In(this=x_dot_key, expressions=keys_to_delete).not_(),
3871            expressions=[exp.to_identifier("x")],
3872        )
3873        result = exp.func(
3874            "MAP_FROM_ENTRIES",
3875            exp.ArrayFilter(this=exp.func("MAP_ENTRIES", map_arg), expression=lambda_expr),
3876        )
3877        return self.sql(result)
def mappick_sql(self, expression: sqlglot.expressions.array.MapPick) -> str:
3879    def mappick_sql(self, expression: exp.MapPick) -> str:
3880        map_arg = expression.this
3881        keys_to_pick = expression.expressions
3882
3883        x_dot_key = exp.Dot(this=exp.to_identifier("x"), expression=exp.to_identifier("key"))
3884
3885        if len(keys_to_pick) == 1 and keys_to_pick[0].is_type(exp.DType.ARRAY):
3886            lambda_expr = exp.Lambda(
3887                this=exp.func("ARRAY_CONTAINS", keys_to_pick[0], x_dot_key),
3888                expressions=[exp.to_identifier("x")],
3889            )
3890        else:
3891            lambda_expr = exp.Lambda(
3892                this=exp.In(this=x_dot_key, expressions=keys_to_pick),
3893                expressions=[exp.to_identifier("x")],
3894            )
3895
3896        result = exp.func(
3897            "MAP_FROM_ENTRIES",
3898            exp.func("LIST_FILTER", exp.func("MAP_ENTRIES", map_arg), lambda_expr),
3899        )
3900        return self.sql(result)
def mapsize_sql(self, expression: sqlglot.expressions.array.MapSize) -> str:
3902    def mapsize_sql(self, expression: exp.MapSize) -> str:
3903        return self.func("CARDINALITY", expression.this)
@unsupported_args('update_flag')
def mapinsert_sql(self, expression: sqlglot.expressions.array.MapInsert) -> str:
3905    @unsupported_args("update_flag")
3906    def mapinsert_sql(self, expression: exp.MapInsert) -> str:
3907        map_arg = expression.this
3908        key = expression.args.get("key")
3909        value = expression.args.get("value")
3910
3911        map_type = map_arg.type
3912
3913        if value is not None:
3914            if map_type and map_type.expressions and len(map_type.expressions) > 1:
3915                # Extract the value type from MAP(key_type, value_type)
3916                value_type = map_type.expressions[1]
3917                # Cast value to match the map's value type to avoid type conflicts
3918                value = exp.cast(value, value_type)
3919            # else: polymorphic MAP case - no type parameters available, use value as-is
3920
3921        # Create a single-entry map for the new key-value pair
3922        new_entry_struct = exp.Struct(expressions=[exp.PropertyEQ(this=key, expression=value)])
3923        new_entry: exp.Expression = exp.ToMap(this=new_entry_struct)
3924
3925        # Use MAP_CONCAT to merge the original map with the new entry
3926        # This automatically handles both insert and update cases
3927        result = exp.func("MAP_CONCAT", map_arg, new_entry)
3928
3929        return self.sql(result)
def startswith_sql(self, expression: sqlglot.expressions.string.StartsWith) -> str:
3931    def startswith_sql(self, expression: exp.StartsWith) -> str:
3932        return self.func(
3933            "STARTS_WITH",
3934            _cast_to_varchar(expression.this),
3935            _cast_to_varchar(expression.expression),
3936        )
def space_sql(self, expression: sqlglot.expressions.string.Space) -> str:
3938    def space_sql(self, expression: exp.Space) -> str:
3939        # DuckDB's REPEAT requires BIGINT for the count parameter
3940        return self.sql(
3941            exp.Repeat(
3942                this=exp.Literal.string(" "),
3943                times=exp.cast(expression.this, exp.DType.BIGINT),
3944            )
3945        )
def tablefromrows_sql(self, expression: sqlglot.expressions.query.TableFromRows) -> str:
3947    def tablefromrows_sql(self, expression: exp.TableFromRows) -> str:
3948        # For GENERATOR, unwrap TABLE() - just emit the Generator (becomes RANGE)
3949        if isinstance(expression.this, exp.Generator):
3950            # Preserve alias, joins, and other table-level args
3951            table = exp.Table(
3952                this=expression.this,
3953                alias=expression.args.get("alias"),
3954                joins=expression.args.get("joins"),
3955            )
3956            return self.sql(table)
3957
3958        return super().tablefromrows_sql(expression)
def unnest_sql(self, expression: sqlglot.expressions.array.Unnest) -> str:
3960    def unnest_sql(self, expression: exp.Unnest) -> str:
3961        explode_array = expression.args.get("explode_array")
3962        if explode_array:
3963            # In BigQuery, UNNESTing a nested array leads to explosion of the top-level array & struct
3964            # This is transpiled to DDB by transforming "FROM UNNEST(...)" to "FROM (SELECT UNNEST(..., max_depth => 2))"
3965            expression.expressions.append(
3966                exp.Kwarg(this=exp.var("max_depth"), expression=exp.Literal.number(2))
3967            )
3968
3969            # If BQ's UNNEST is aliased, we transform it from a column alias to a table alias in DDB
3970            alias = expression.args.get("alias")
3971            if isinstance(alias, exp.TableAlias):
3972                expression.set("alias", None)
3973                if alias.columns:
3974                    alias = exp.TableAlias(this=seq_get(alias.columns, 0))
3975
3976            unnest_sql = super().unnest_sql(expression)
3977            select = exp.Select(expressions=[unnest_sql]).subquery(alias)
3978            return self.sql(select)
3979
3980        return super().unnest_sql(expression)
def arrayagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayAgg) -> str:
3982    def arrayagg_sql(self, expression: exp.ArrayAgg) -> str:
3983        if isinstance(expression.this, exp.Limit):
3984            self.unsupported("LIMIT inside ARRAY_AGG is not supported in DuckDB")
3985
3986        return super().arrayagg_sql(expression)
def ignorenulls_sql(self, expression: sqlglot.expressions.core.IgnoreNulls) -> str:
3988    def ignorenulls_sql(self, expression: exp.IgnoreNulls) -> str:
3989        this = expression.this
3990
3991        if isinstance(this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
3992            # DuckDB should render IGNORE NULLS only for the general-purpose
3993            # window functions that accept it e.g. FIRST_VALUE(... IGNORE NULLS) OVER (...)
3994            return super().ignorenulls_sql(expression)
3995
3996        # For ARRAY_AGG(expr IGNORE NULLS ...), convert IGNORE NULLS to a
3997        # FILTER(WHERE expr IS NOT NULL) clause by setting nulls_excluded on
3998        # the ArrayAgg.  The existing _add_arrayagg_null_filter method will
3999        # emit the FILTER clause during arrayagg_sql / withingroup_sql.
4000        if isinstance(this, exp.ArrayAgg):
4001            this.set("nulls_excluded", True)
4002            return self.sql(this)
4003
4004        if isinstance(this, exp.First):
4005            this = exp.AnyValue(this=this.this)
4006
4007        if not isinstance(this, (exp.AnyValue, exp.ApproxQuantiles)):
4008            self.unsupported("IGNORE NULLS is not supported for non-window functions.")
4009
4010        return self.sql(this)
def split_sql(self, expression: sqlglot.expressions.string.Split) -> str:
4012    def split_sql(self, expression: exp.Split) -> str:
4013        base_func = exp.func("STR_SPLIT", expression.this, expression.expression)
4014
4015        case_expr = exp.case().else_(base_func)
4016        needs_case = False
4017
4018        if expression.args.get("null_returns_null"):
4019            case_expr = case_expr.when(expression.expression.is_(exp.null()), exp.null())
4020            needs_case = True
4021
4022        if expression.args.get("empty_delimiter_returns_whole"):
4023            # When delimiter is empty string, return input string as single array element
4024            array_with_input = exp.array(expression.this)
4025            case_expr = case_expr.when(
4026                expression.expression.eq(exp.Literal.string("")), array_with_input
4027            )
4028            needs_case = True
4029
4030        return self.sql(case_expr if needs_case else base_func)
def splitpart_sql(self, expression: sqlglot.expressions.string.SplitPart) -> str:
4032    def splitpart_sql(self, expression: exp.SplitPart) -> str:
4033        string_arg = expression.this
4034        delimiter_arg = expression.args.get("delimiter")
4035        part_index_arg = expression.args.get("part_index")
4036
4037        if delimiter_arg and part_index_arg:
4038            # Handle Snowflake's "index 0 and 1 both return first element" behavior
4039            if expression.args.get("part_index_zero_as_one"):
4040                # Convert 0 to 1 for compatibility
4041
4042                part_index_arg = exp.Paren(
4043                    this=exp.case()
4044                    .when(part_index_arg.eq(exp.Literal.number("0")), exp.Literal.number("1"))
4045                    .else_(part_index_arg)
4046                )
4047
4048            # Use Anonymous to avoid recursion
4049            base_func_expr: exp.Expr = exp.Anonymous(
4050                this="SPLIT_PART", expressions=[string_arg, delimiter_arg, part_index_arg]
4051            )
4052            needs_case_transform = False
4053            case_expr = exp.case().else_(base_func_expr)
4054
4055            if expression.args.get("empty_delimiter_returns_whole"):
4056                # When delimiter is empty string:
4057                # - Return whole string if part_index is 1 or -1
4058                # - Return empty string otherwise
4059                empty_case = exp.Paren(
4060                    this=exp.case()
4061                    .when(
4062                        exp.or_(
4063                            part_index_arg.eq(exp.Literal.number("1")),
4064                            part_index_arg.eq(exp.Literal.number("-1")),
4065                        ),
4066                        string_arg,
4067                    )
4068                    .else_(exp.Literal.string(""))
4069                )
4070
4071                case_expr = case_expr.when(delimiter_arg.eq(exp.Literal.string("")), empty_case)
4072                needs_case_transform = True
4073
4074            """
4075            Output looks something like this:
4076
4077            CASE
4078            WHEN delimiter is '' THEN
4079                (
4080                    CASE
4081                    WHEN adjusted_part_index = 1 OR adjusted_part_index = -1 THEN input
4082                    ELSE '' END
4083                )
4084            ELSE SPLIT_PART(input, delimiter, adjusted_part_index)
4085            END
4086
4087            """
4088            return self.sql(case_expr if needs_case_transform else base_func_expr)
4089
4090        return self.function_fallback_sql(expression)
def respectnulls_sql(self, expression: sqlglot.expressions.core.RespectNulls) -> str:
4092    def respectnulls_sql(self, expression: exp.RespectNulls) -> str:
4093        if isinstance(expression.this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
4094            # DuckDB should render RESPECT NULLS only for the general-purpose
4095            # window functions that accept it e.g. FIRST_VALUE(... RESPECT NULLS) OVER (...)
4096            return super().respectnulls_sql(expression)
4097
4098        self.unsupported("RESPECT NULLS is not supported for non-window functions.")
4099        return self.sql(expression, "this")
def arraytostring_sql(self, expression: sqlglot.expressions.array.ArrayToString) -> str:
4101    def arraytostring_sql(self, expression: exp.ArrayToString) -> str:
4102        null = expression.args.get("null")
4103
4104        if expression.args.get("null_is_empty"):
4105            x = exp.to_identifier("x")
4106            list_transform = exp.Transform(
4107                this=expression.this.copy(),
4108                expression=exp.Lambda(
4109                    this=exp.Coalesce(
4110                        this=exp.cast(x, "TEXT"), expressions=[exp.Literal.string("")]
4111                    ),
4112                    expressions=[x],
4113                ),
4114            )
4115            array_to_string = exp.ArrayToString(
4116                this=list_transform, expression=expression.expression
4117            )
4118            if expression.args.get("null_delim_is_null"):
4119                return self.sql(
4120                    exp.case()
4121                    .when(expression.expression.copy().is_(exp.null()), exp.null())
4122                    .else_(array_to_string)
4123                )
4124            return self.sql(array_to_string)
4125
4126        if null:
4127            x = exp.to_identifier("x")
4128            return self.sql(
4129                exp.ArrayToString(
4130                    this=exp.Transform(
4131                        this=expression.this,
4132                        expression=exp.Lambda(
4133                            this=exp.Coalesce(this=x, expressions=[null]),
4134                            expressions=[x],
4135                        ),
4136                    ),
4137                    expression=expression.expression,
4138                )
4139            )
4140
4141        return self.func("ARRAY_TO_STRING", expression.this, expression.expression)
def concatws_sql(self, expression: sqlglot.expressions.string.ConcatWs) -> str:
4143    def concatws_sql(self, expression: exp.ConcatWs) -> str:
4144        # DuckDB-specific: handle binary types using DPipe (||) operator
4145        separator = seq_get(expression.expressions, 0)
4146        args = expression.expressions[1:]
4147
4148        if any(_is_binary(arg) for arg in [separator, *args]):
4149            result = args[0]
4150            for arg in args[1:]:
4151                result = exp.DPipe(
4152                    this=exp.DPipe(this=result, expression=separator), expression=arg
4153                )
4154            return self.sql(result)
4155
4156        return super().concatws_sql(expression)
def regexpextract_sql(self, expression: sqlglot.expressions.string.RegexpExtract) -> str:
4211    def regexpextract_sql(self, expression: exp.RegexpExtract) -> str:
4212        return self._regexp_extract_sql(expression)
def regexpextractall_sql(self, expression: sqlglot.expressions.string.RegexpExtractAll) -> str:
4214    def regexpextractall_sql(self, expression: exp.RegexpExtractAll) -> str:
4215        return self._regexp_extract_sql(expression)
def regexpinstr_sql(self, expression: sqlglot.expressions.string.RegexpInstr) -> str:
4217    def regexpinstr_sql(self, expression: exp.RegexpInstr) -> str:
4218        this = expression.this
4219        pattern = expression.expression
4220        position = expression.args.get("position")
4221        orig_occ = expression.args.get("occurrence")
4222        occurrence = orig_occ or exp.Literal.number(1)
4223        option = expression.args.get("option")
4224        parameters = expression.args.get("parameters")
4225
4226        validated_flags = self._validate_regexp_flags(parameters, supported_flags="ims")
4227        if validated_flags:
4228            pattern = exp.Concat(expressions=[exp.Literal.string(f"(?{validated_flags})"), pattern])
4229
4230        # Handle starting position offset
4231        pos_offset: exp.Expr = exp.Literal.number(0)
4232        if position and (not position.is_int or position.to_py() > 1):
4233            this = exp.Substring(this=this, start=position)
4234            pos_offset = position - exp.Literal.number(1)
4235
4236        # Helper: LIST_SUM(LIST_TRANSFORM(list[1:end], x -> LENGTH(x)))
4237        def sum_lengths(func_name: str, end: exp.Expr) -> exp.Expr:
4238            lst = exp.Bracket(
4239                this=exp.Anonymous(this=func_name, expressions=[this, pattern]),
4240                expressions=[exp.Slice(this=exp.Literal.number(1), expression=end)],
4241                offset=1,
4242            )
4243            transform = exp.Anonymous(
4244                this="LIST_TRANSFORM",
4245                expressions=[
4246                    lst,
4247                    exp.Lambda(
4248                        this=exp.Length(this=exp.to_identifier("x")),
4249                        expressions=[exp.to_identifier("x")],
4250                    ),
4251                ],
4252            )
4253            return exp.Coalesce(
4254                this=exp.Anonymous(this="LIST_SUM", expressions=[transform]),
4255                expressions=[exp.Literal.number(0)],
4256            )
4257
4258        # Position = 1 + sum(split_lengths[1:occ]) + sum(match_lengths[1:occ-1]) + offset
4259        base_pos: exp.Expr = (
4260            exp.Literal.number(1)
4261            + sum_lengths("STRING_SPLIT_REGEX", occurrence)
4262            + sum_lengths("REGEXP_EXTRACT_ALL", occurrence - exp.Literal.number(1))
4263            + pos_offset
4264        )
4265
4266        # option=1: add match length for end position
4267        if option and option.is_int and option.to_py() == 1:
4268            match_at_occ = exp.Bracket(
4269                this=exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern]),
4270                expressions=[occurrence],
4271                offset=1,
4272            )
4273            base_pos = base_pos + exp.Coalesce(
4274                this=exp.Length(this=match_at_occ), expressions=[exp.Literal.number(0)]
4275            )
4276
4277        # NULL checks for all provided arguments
4278        # .copy() is used strictly because .is_() alters the node's parent pointer, mutating the parsed AST
4279        null_args = [
4280            expression.this,
4281            expression.expression,
4282            position,
4283            orig_occ,
4284            option,
4285            parameters,
4286        ]
4287        null_checks = [arg.copy().is_(exp.Null()) for arg in null_args if arg]
4288
4289        matches = exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern])
4290
4291        return self.sql(
4292            exp.case()
4293            .when(exp.or_(*null_checks), exp.Null())
4294            .when(pattern.copy().eq(exp.Literal.string("")), exp.Literal.number(0))
4295            .when(exp.Length(this=matches) < occurrence, exp.Literal.number(0))
4296            .else_(base_pos)
4297        )
@unsupported_args('culture')
def numbertostr_sql(self, expression: sqlglot.expressions.string.NumberToStr) -> str:
4299    @unsupported_args("culture")
4300    def numbertostr_sql(self, expression: exp.NumberToStr) -> str:
4301        fmt = expression.args.get("format")
4302        if fmt and fmt.is_int:
4303            return self.func("FORMAT", f"'{{:,.{fmt.name}f}}'", expression.this)
4304
4305        self.unsupported("Only integer formats are supported by NumberToStr")
4306        return self.function_fallback_sql(expression)
def autoincrementcolumnconstraint_sql(self, _) -> str:
4308    def autoincrementcolumnconstraint_sql(self, _) -> str:
4309        self.unsupported("The AUTOINCREMENT column constraint is not supported by DuckDB")
4310        return ""
def aliases_sql(self, expression: sqlglot.expressions.core.Aliases) -> str:
4312    def aliases_sql(self, expression: exp.Aliases) -> str:
4313        this = expression.this
4314        if isinstance(this, exp.Posexplode):
4315            return self.posexplode_sql(this)
4316
4317        return super().aliases_sql(expression)
def posexplode_sql(self, expression: sqlglot.expressions.array.Posexplode) -> str:
4319    def posexplode_sql(self, expression: exp.Posexplode) -> str:
4320        this = expression.this
4321        parent = expression.parent
4322
4323        # The default Spark aliases are "pos" and "col", unless specified otherwise
4324        pos, col = exp.to_identifier("pos"), exp.to_identifier("col")
4325
4326        if isinstance(parent, exp.Aliases):
4327            # Column case: SELECT POSEXPLODE(col) [AS (a, b)]
4328            pos, col = parent.expressions
4329        elif isinstance(parent, exp.Table):
4330            # Table case: SELECT * FROM POSEXPLODE(col) [AS (a, b)]
4331            alias = parent.args.get("alias")
4332            if alias:
4333                pos, col = alias.columns or [pos, col]
4334                alias.pop()
4335
4336        # Translate POSEXPLODE to UNNEST + GENERATE_SUBSCRIPTS
4337        # Note: In Spark pos is 0-indexed, but in DuckDB it's 1-indexed, so we subtract 1 from GENERATE_SUBSCRIPTS
4338        unnest_sql = self.sql(exp.Unnest(expressions=[this], alias=col))
4339        gen_subscripts = self.sql(
4340            exp.Alias(
4341                this=exp.Anonymous(
4342                    this="GENERATE_SUBSCRIPTS", expressions=[this, exp.Literal.number(1)]
4343                )
4344                - exp.Literal.number(1),
4345                alias=pos,
4346            )
4347        )
4348
4349        posexplode_sql = self.format_args(gen_subscripts, unnest_sql)
4350
4351        if isinstance(parent, exp.From) or (parent and isinstance(parent.parent, exp.From)):
4352            # SELECT * FROM POSEXPLODE(col) -> SELECT * FROM (SELECT GENERATE_SUBSCRIPTS(...), UNNEST(...))
4353            return self.sql(exp.Subquery(this=exp.Select(expressions=[posexplode_sql])))
4354
4355        return posexplode_sql
def addmonths_sql(self, expression: sqlglot.expressions.temporal.AddMonths) -> str:
4357    def addmonths_sql(self, expression: exp.AddMonths) -> str:
4358        """
4359        Handles three key issues:
4360        1. Float/decimal months: e.g., Snowflake rounds, whereas DuckDB INTERVAL requires integers
4361        2. End-of-month preservation: If input is last day of month, result is last day of result month
4362        3. Type preservation: Maintains DATE/TIMESTAMPTZ types (DuckDB defaults to TIMESTAMP)
4363        """
4364        from sqlglot.optimizer.annotate_types import annotate_types
4365
4366        this = expression.this
4367        if not this.type:
4368            this = annotate_types(this, dialect=self.dialect)
4369
4370        if this.is_type(*exp.DataType.TEXT_TYPES):
4371            this = exp.Cast(this=this, to=exp.DataType(this=exp.DType.TIMESTAMP))
4372
4373        # Detect float/decimal months to apply rounding (Snowflake behavior)
4374        # DuckDB INTERVAL syntax doesn't support non-integer expressions, so use TO_MONTHS
4375        months_expr = expression.expression
4376        if not months_expr.type:
4377            months_expr = annotate_types(months_expr, dialect=self.dialect)
4378
4379        # Build interval or to_months expression based on type
4380        # Float/decimal case: Round and use TO_MONTHS(CAST(ROUND(value) AS INT))
4381        interval_or_to_months = (
4382            exp.func("TO_MONTHS", exp.cast(exp.func("ROUND", months_expr), "INT"))
4383            if months_expr.is_type(
4384                exp.DType.FLOAT,
4385                exp.DType.DOUBLE,
4386                exp.DType.DECIMAL,
4387            )
4388            # Integer case: standard INTERVAL N MONTH syntax
4389            else exp.Interval(this=months_expr, unit=exp.var("MONTH"))
4390        )
4391
4392        date_add_expr = exp.Add(this=this, expression=interval_or_to_months)
4393
4394        # Apply end-of-month preservation if Snowflake flag is set
4395        # CASE WHEN LAST_DAY(date) = date THEN LAST_DAY(result) ELSE result END
4396        preserve_eom = expression.args.get("preserve_end_of_month")
4397        result_expr = (
4398            exp.case()
4399            .when(
4400                exp.EQ(this=exp.func("LAST_DAY", this), expression=this),
4401                exp.func("LAST_DAY", date_add_expr),
4402            )
4403            .else_(date_add_expr)
4404            if preserve_eom
4405            else date_add_expr
4406        )
4407
4408        # DuckDB's DATE_ADD function returns TIMESTAMP/DATETIME by default, even when the input is DATE
4409        # To match for example Snowflake's ADD_MONTHS behavior (which preserves the input type)
4410        # We need to cast the result back to the original type when the input is DATE or TIMESTAMPTZ
4411        # Example: ADD_MONTHS('2023-01-31'::date, 1) should return DATE, not TIMESTAMP
4412        if this.is_type(exp.DType.DATE, exp.DType.TIMESTAMPTZ):
4413            return self.sql(exp.Cast(this=result_expr, to=this.type))
4414        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:
4416    def format_sql(self, expression: exp.Format) -> str:
4417        if expression.name.lower() == "%s" and len(expression.expressions) == 1:
4418            return self.func("FORMAT", "'{}'", expression.expressions[0])
4419
4420        return self.function_fallback_sql(expression)
def hexstring_sql( self, expression: sqlglot.expressions.query.HexString, binary_function_repr: str | None = None) -> str:
4422    def hexstring_sql(
4423        self, expression: exp.HexString, binary_function_repr: str | None = None
4424    ) -> str:
4425        # UNHEX('FF') correctly produces blob \xFF in DuckDB
4426        return super().hexstring_sql(expression, binary_function_repr="UNHEX")
def datetrunc_sql(self, expression: sqlglot.expressions.temporal.DateTrunc) -> str:
4428    def datetrunc_sql(self, expression: exp.DateTrunc) -> str:
4429        unit = expression.args.get("unit")
4430        date = expression.this
4431
4432        week_start = _week_trunc_start_dow(unit)
4433        unit = unit_to_str(expression)
4434
4435        if week_start:
4436            result = self.sql(
4437                _build_week_trunc_expression(date, week_start, preserve_start_day=True)
4438            )
4439        else:
4440            result = self.func("DATE_TRUNC", unit, date)
4441
4442        if (
4443            expression.args.get("input_type_preserved")
4444            and date.is_type(*exp.DataType.TEMPORAL_TYPES)
4445            and not (is_date_unit(unit) and date.is_type(exp.DType.DATE))
4446        ):
4447            return self.sql(exp.Cast(this=result, to=date.type))
4448
4449        return result
def datetimetrunc_sql(self, expression: sqlglot.expressions.temporal.DatetimeTrunc) -> str:
4451    def datetimetrunc_sql(self, expression: exp.DatetimeTrunc) -> str:
4452        this = exp.cast(expression.this, exp.DType.DATETIME)
4453        week_start = _week_trunc_start_dow(expression.args.get("unit"))
4454        if week_start:
4455            return self.sql(
4456                _build_week_trunc_expression(
4457                    this, week_start, preserve_start_day=True, cast_to_date=False
4458                )
4459            )
4460
4461        return self.func("DATE_TRUNC", unit_to_str(expression), this)
def timestamptrunc_sql(self, expression: sqlglot.expressions.temporal.TimestampTrunc) -> str:
4463    def timestamptrunc_sql(self, expression: exp.TimestampTrunc) -> str:
4464        zone = expression.args.get("zone")
4465        timestamp = expression.this
4466        week_start = _week_trunc_start_dow(expression.args.get("unit"))
4467
4468        # The week start emulation below is exact, so avoid weekstart_unit_to_str's degrade warning
4469        unit = unit_to_str(expression) if week_start else weekstart_unit_to_str(self, expression)
4470        date_unit = is_date_unit(unit) or bool(week_start)
4471
4472        def _trunc_expr(this: exp.Expr) -> exp.Expr:
4473            if week_start:
4474                return _build_week_trunc_expression(
4475                    this, week_start, preserve_start_day=True, cast_to_date=False
4476                )
4477            return exp.func("DATE_TRUNC", unit, this)
4478
4479        if date_unit and zone:
4480            # BigQuery's TIMESTAMP_TRUNC with timezone truncates in the target timezone and returns as UTC.
4481            # Double AT TIME ZONE needed for BigQuery compatibility:
4482            # 1. First AT TIME ZONE: ensures truncation happens in the target timezone
4483            # 2. Second AT TIME ZONE: converts the DATE result back to TIMESTAMPTZ (preserving time component)
4484            timestamp = exp.AtTimeZone(this=timestamp, zone=zone)
4485            trunced = _trunc_expr(timestamp)
4486            if isinstance(trunced, exp.DateAdd):
4487                # Parenthesize so the trailing AT TIME ZONE binds to the whole shifted expression
4488                trunced = exp.Paren(this=trunced)
4489            return self.sql(exp.AtTimeZone(this=trunced, zone=zone))
4490
4491        result = self.sql(_trunc_expr(timestamp))
4492        if expression.args.get("input_type_preserved"):
4493            if timestamp.type and timestamp.is_type(exp.DType.TIME, exp.DType.TIMETZ):
4494                dummy_date = exp.Cast(
4495                    this=exp.Literal.string("1970-01-01"),
4496                    to=exp.DataType(this=exp.DType.DATE),
4497                )
4498                date_time = exp.Add(this=dummy_date, expression=timestamp)
4499                result = self.func("DATE_TRUNC", unit, date_time)
4500                return self.sql(exp.Cast(this=result, to=timestamp.type))
4501
4502            if timestamp.is_type(*exp.DataType.TEMPORAL_TYPES) and not (
4503                date_unit and timestamp.is_type(exp.DType.DATE)
4504            ):
4505                return self.sql(exp.Cast(this=result, to=timestamp.type))
4506
4507        return result
def trim_sql(self, expression: sqlglot.expressions.string.Trim) -> str:
4509    def trim_sql(self, expression: exp.Trim) -> str:
4510        expression.this.replace(_cast_to_varchar(expression.this))
4511        if expression.expression:
4512            expression.expression.replace(_cast_to_varchar(expression.expression))
4513
4514        result_sql = super().trim_sql(expression)
4515        return _gen_with_cast_to_blob(self, expression, result_sql)
def round_sql(self, expression: sqlglot.expressions.math.Round) -> str:
4517    def round_sql(self, expression: exp.Round) -> str:
4518        this = expression.this
4519        decimals = expression.args.get("decimals")
4520        truncate = expression.args.get("truncate")
4521
4522        # DuckDB requires the scale (decimals) argument to be an INT
4523        # Some dialects (e.g., Snowflake) allow non-integer scales and cast to an integer internally
4524        if decimals is not None and expression.args.get("casts_non_integer_decimals"):
4525            if not (decimals.is_int or decimals.is_type(*exp.DataType.INTEGER_TYPES)):
4526                decimals = exp.cast(decimals, exp.DType.INT)
4527
4528        func = "ROUND"
4529        if truncate:
4530            # BigQuery uses ROUND_HALF_EVEN; Snowflake uses HALF_TO_EVEN
4531            if truncate.this in ("ROUND_HALF_EVEN", "HALF_TO_EVEN"):
4532                func = "ROUND_EVEN"
4533                truncate = None
4534            # BigQuery uses ROUND_HALF_AWAY_FROM_ZERO; Snowflake uses HALF_AWAY_FROM_ZERO
4535            elif truncate.this in ("ROUND_HALF_AWAY_FROM_ZERO", "HALF_AWAY_FROM_ZERO"):
4536                truncate = None
4537
4538        return self.func(func, this, decimals, truncate)
def trycast_sql(self, expression: sqlglot.expressions.functions.TryCast) -> str:
4540    def trycast_sql(self, expression: exp.TryCast) -> str:
4541        to = expression.to
4542        to_type = to.this
4543        src = expression.this
4544
4545        if (
4546            expression.args.get("null_on_text_overflow")
4547            and to_type in exp.DataType.TEXT_TYPES
4548            and to.expressions
4549        ):
4550            return self.sql(
4551                exp.case()
4552                .when(
4553                    exp.LTE(this=exp.func("LENGTH", src), expression=to.expressions[0].this),
4554                    exp.cast(src, "TEXT"),
4555                )
4556                .else_(exp.Null())
4557            )
4558        elif to_type == exp.DType.DATE and expression.args.get("probe_date_format"):
4559            slash_strptime = exp.cast(
4560                exp.func("TRY_STRPTIME", src, exp.Literal.string(self._TRYCAST_DATE_SLASH_FMT)),
4561                "DATE",
4562            )
4563            mon_strptime = exp.cast(
4564                exp.func("TRY_STRPTIME", src, exp.Literal.string(self._TRYCAST_DATE_MON_FMT)),
4565                "DATE",
4566            )
4567            return self.sql(
4568                exp.case()
4569                .when(exp.func("CONTAINS", src, exp.Literal.string("/")), slash_strptime)
4570                .when(
4571                    exp.RegexpLike(this=src, expression=exp.Literal.string("[A-Za-z]")),
4572                    mon_strptime,
4573                )
4574                .else_(exp.TryCast(this=src, to=to))
4575            )
4576        elif (
4577            isinstance(to_type, exp.Interval)
4578            and (unit := to_type.unit)
4579            and expression.args.get("requires_string")
4580        ):
4581            interval_type = exp.DataType.build("INTERVAL")
4582            if isinstance(unit, exp.IntervalSpan):
4583                self.unsupported(
4584                    "TRY_CAST to INTERVAL with span (e.g. HOUR TO MINUTE) is not supported in DuckDB"
4585                )
4586                return self.sql(exp.TryCast(this=src, to=interval_type))
4587            return self.sql(
4588                exp.TryCast(
4589                    this=exp.DPipe(this=src, expression=exp.Literal.string(f" {unit.name}")),
4590                    to=interval_type,
4591                )
4592            )
4593
4594        return super().trycast_sql(expression)
def strtok_sql(self, expression: sqlglot.expressions.string.Strtok) -> str:
4596    def strtok_sql(self, expression: exp.Strtok) -> str:
4597        string_arg = expression.this
4598        delimiter_arg = expression.args.get("delimiter")
4599        part_index_arg = expression.args.get("part_index")
4600
4601        if delimiter_arg and part_index_arg:
4602            # Escape regex chars and build character class at runtime using REGEXP_REPLACE
4603            escaped_delimiter = exp.Anonymous(
4604                this="REGEXP_REPLACE",
4605                expressions=[
4606                    delimiter_arg,
4607                    exp.Literal.string(
4608                        r"([\[\]^.\-*+?(){}|$\\])"
4609                    ),  # Escape problematic regex chars
4610                    exp.Literal.string(
4611                        r"\\\1"
4612                    ),  # Replace with escaped version using $1 backreference
4613                    exp.Literal.string("g"),  # Global flag
4614                ],
4615            )
4616            # CASE WHEN delimiter = '' THEN '' ELSE CONCAT('[', escaped_delimiter, ']') END
4617            regex_pattern = (
4618                exp.case()
4619                .when(delimiter_arg.eq(exp.Literal.string("")), exp.Literal.string(""))
4620                .else_(
4621                    exp.func(
4622                        "CONCAT",
4623                        exp.Literal.string("["),
4624                        escaped_delimiter,
4625                        exp.Literal.string("]"),
4626                    )
4627                )
4628            )
4629
4630            # STRTOK skips empty strings, so we need to filter them out
4631            # LIST_FILTER(REGEXP_SPLIT_TO_ARRAY(string, pattern), x -> x != '')[index]
4632            split_array = exp.func("REGEXP_SPLIT_TO_ARRAY", string_arg, regex_pattern)
4633            x = exp.to_identifier("x")
4634            is_empty = x.eq(exp.Literal.string(""))
4635            filtered_array = exp.func(
4636                "LIST_FILTER",
4637                split_array,
4638                exp.Lambda(this=exp.not_(is_empty.copy()), expressions=[x.copy()]),
4639            )
4640            base_func = exp.Bracket(
4641                this=filtered_array,
4642                expressions=[part_index_arg],
4643                offset=1,
4644            )
4645
4646            # Use template with the built regex pattern
4647            result = exp.replace_placeholders(
4648                self.STRTOK_TEMPLATE.copy(),
4649                string=string_arg,
4650                delimiter=delimiter_arg,
4651                part_index=part_index_arg,
4652                base_func=base_func,
4653            )
4654
4655            return self.sql(result)
4656
4657        return self.function_fallback_sql(expression)
def strtoktoarray_sql(self, expression: sqlglot.expressions.array.StrtokToArray) -> str:
4659    def strtoktoarray_sql(self, expression: exp.StrtokToArray) -> str:
4660        string_arg = expression.this
4661        delimiter_arg = expression.args.get("expression") or exp.Literal.string(" ")
4662
4663        escaped = exp.RegexpReplace(
4664            this=delimiter_arg.copy(),
4665            expression=exp.Literal.string(r"([\[\]^.\-*+?(){}|$\\])"),
4666            replacement=exp.Literal.string(r"\\\1"),
4667            modifiers=exp.Literal.string("g"),
4668        )
4669        return self.sql(
4670            exp.replace_placeholders(
4671                self.STRTOK_TO_ARRAY_TEMPLATE.copy(),
4672                string=string_arg,
4673                delimiter=delimiter_arg,
4674                escaped=escaped,
4675            )
4676        )
def approxquantile_sql(self, expression: sqlglot.expressions.aggregate.ApproxQuantile) -> str:
4678    def approxquantile_sql(self, expression: exp.ApproxQuantile) -> str:
4679        result = self.func("APPROX_QUANTILE", expression.this, expression.args.get("quantile"))
4680
4681        # DuckDB returns integers for APPROX_QUANTILE, cast to DOUBLE if the expected type is a real type
4682        if expression.is_type(*exp.DataType.REAL_TYPES):
4683            result = f"CAST({result} AS DOUBLE)"
4684
4685        return result
def approxquantiles_sql(self, expression: sqlglot.expressions.aggregate.ApproxQuantiles) -> str:
4687    def approxquantiles_sql(self, expression: exp.ApproxQuantiles) -> str:
4688        """
4689        BigQuery's APPROX_QUANTILES(expr, n) returns an array of n+1 approximate quantile values
4690        dividing the input distribution into n equal-sized buckets.
4691
4692        Both BigQuery and DuckDB use approximate algorithms for quantile estimation, but BigQuery
4693        does not document the specific algorithm used so results may differ. DuckDB does not
4694        support RESPECT NULLS.
4695        """
4696        this = expression.this
4697        if isinstance(this, exp.Distinct):
4698            # APPROX_QUANTILES requires 2 args and DISTINCT node grabs both
4699            if len(this.expressions) < 2:
4700                self.unsupported("APPROX_QUANTILES requires a bucket count argument")
4701                return self.function_fallback_sql(expression)
4702            num_quantiles_expr = this.expressions[1].pop()
4703        else:
4704            num_quantiles_expr = expression.expression
4705
4706        if not isinstance(num_quantiles_expr, exp.Literal) or not num_quantiles_expr.is_int:
4707            self.unsupported("APPROX_QUANTILES bucket count must be a positive integer")
4708            return self.function_fallback_sql(expression)
4709
4710        num_quantiles = t.cast(int, num_quantiles_expr.to_py())
4711        if num_quantiles <= 0:
4712            self.unsupported("APPROX_QUANTILES bucket count must be a positive integer")
4713            return self.function_fallback_sql(expression)
4714
4715        quantiles = [
4716            exp.Literal.number(Decimal(i) / Decimal(num_quantiles))
4717            for i in range(num_quantiles + 1)
4718        ]
4719
4720        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:
4722    def jsonextractscalar_sql(self, expression: exp.JSONExtractScalar) -> str:
4723        if expression.args.get("scalar_only"):
4724            expression = exp.JSONExtractScalar(
4725                this=rename_func("JSON_VALUE")(self, expression), expression="'$'"
4726            )
4727        return _arrow_json_extract_sql(self, expression)
def bitwisenot_sql(self, expression: sqlglot.expressions.core.BitwiseNot) -> str:
4729    def bitwisenot_sql(self, expression: exp.BitwiseNot) -> str:
4730        this = expression.this
4731
4732        if _is_binary(this):
4733            expression.type = exp.DType.BINARY.into_expr()
4734
4735        arg = _cast_to_bit(this)
4736
4737        if isinstance(this, exp.Neg):
4738            arg = exp.Paren(this=arg)
4739
4740        expression.set("this", arg)
4741
4742        result_sql = f"~{self.sql(expression, 'this')}"
4743
4744        return _gen_with_cast_to_blob(self, expression, result_sql)
def window_sql(self, expression: sqlglot.expressions.query.Window) -> str:
4746    def window_sql(self, expression: exp.Window) -> str:
4747        this = expression.this
4748        if isinstance(this, exp.Corr) or (
4749            isinstance(this, exp.Filter) and isinstance(this.this, exp.Corr)
4750        ):
4751            return self._corr_sql(expression)
4752
4753        return super().window_sql(expression)
def filter_sql(self, expression: sqlglot.expressions.core.Filter) -> str:
4755    def filter_sql(self, expression: exp.Filter) -> str:
4756        if isinstance(expression.this, exp.Corr):
4757            return self._corr_sql(expression)
4758
4759        return super().filter_sql(expression)
def uuid_sql(self, expression: sqlglot.expressions.functions.Uuid) -> str:
4778    def uuid_sql(self, expression: exp.Uuid) -> str:
4779        namespace = expression.this
4780        name = expression.args.get("name")
4781
4782        # UUID v5 (namespace + name) - Emulate using SHA1
4783        if namespace and name:
4784            result = exp.replace_placeholders(
4785                self.UUID_V5_TEMPLATE.copy(),
4786                namespace=namespace,
4787                name=name,
4788            )
4789            return self.sql(result)
4790
4791        return super().uuid_sql(expression)
Inherited Members
sqlglot.generator.Generator
Generator
NULL_ORDERING_SUPPORTED
WINDOW_FUNCS_WITH_NULL_ORDERING
LOCKING_READS_SUPPORTED
EXCEPT_INTERSECT_SUPPORT_ALL_CLAUSE
WRAP_DERIVED_VALUES
CREATE_FUNCTION_RETURN_AS
MATCHED_BY_SOURCE
SUPPORTS_MERGE_WHERE
SINGLE_STRING_INTERVAL
INTERVAL_ALLOWS_PLURAL_FORM
AUTO_REFRESH_BARE_INTERVALS
LIMIT_ONLY_LITERALS
GROUPINGS_SEP
INDEX_ON
INOUT_SEPARATOR
DIRECTED_JOINS
QUERY_HINT_SEP
IS_BOOL_ALLOWED
DUPLICATE_KEY_UPDATE_WITH_SET
LIMIT_IS_TOP
RETURNING_END
EXTRACT_ALLOWS_QUOTES
TZ_TO_WITH_TIME_ZONE
VALUES_AS_TABLE
ALTER_TABLE_INCLUDE_COLUMN_KEYWORD
UNNEST_WITH_ORDINALITY
COMPUTED_COLUMN_WITH_TYPE
SUPPORTS_TABLE_COPY
TABLESAMPLE_REQUIRES_PARENS
TABLESAMPLE_SIZE_IS_ROWS
TABLESAMPLE_WITH_METHOD
COLLATE_IS_FUNC
DATA_TYPE_SPECIFIERS_ALLOWED
ENSURE_BOOLS
CTE_RECURSIVE_KEYWORD_REQUIRED
SUPPORTS_SINGLE_ARG_CONCAT
SUPPORTS_TABLE_ALIAS_COLUMNS
SUPPORTS_NAMED_CTE_COLUMNS
UNPIVOT_ALIASES_ARE_IDENTIFIERS
PIVOT_ALIAS_WITH_AS
INSERT_OVERWRITE
SUPPORTS_SELECT_INTO
SUPPORTS_UNLOGGED_TABLES
SUPPORTS_MODIFY_COLUMN
SUPPORTS_CHANGE_COLUMN
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
SAFE_JSON_PATH_KEY_RE
SENTINEL_LINE_BREAK
pretty
identify
normalize
pad
unsupported_level
max_unsupported
leading_comma
max_text_width
comments
dialect
normalize_functions
unsupported_messages
generate
preprocess
unsupported
sep
seg
sanitize_comment
maybe_comment
wrap
no_identify
normalize_func
indent
sql
uncache_sql
cache_sql
characterset_sql
column_parts
column_sql
pseudocolumn_sql
columnposition_sql
columndef_sql
columnconstraint_sql
computedcolumnconstraint_sql
compresscolumnconstraint_sql
generatedasidentitycolumnconstraint_sql
generatedasrowcolumnconstraint_sql
periodforsystemtimeconstraint_sql
notnullcolumnconstraint_sql
primarykeycolumnconstraint_sql
uniquecolumnconstraint_sql
inoutcolumnconstraint_sql
createable_sql
create_sql
sequenceproperties_sql
triggerproperties_sql
triggerreferencing_sql
triggerevent_sql
clone_sql
describe_sql
heredoc_sql
prepend_ctes
with_sql
cte_sql
tablealias_sql
bitstring_sql
bytestring_sql
unicodestring_sql
rawstring_sql
datatypeparam_sql
datatype_param_bound_limiter
datatype_sql
directory_sql
delete_sql
drop_sql
set_operation
set_operations
fetch_sql
limitoptions_sql
hint_sql
indexparameters_sql
index_sql
dynamicidentifier_sql
identifier_sql
lowerhex_sql
inputoutputformat_sql
national_sql
partition_sql
properties_sql
root_properties
properties
with_properties
locate_properties
property_name
property_sql
uuidproperty_sql
likeproperty_sql
fallbackproperty_sql
journalproperty_sql
freespaceproperty_sql
checksumproperty_sql
mergeblockratioproperty_sql
moduleproperty_sql
datablocksizeproperty_sql
blockcompressionproperty_sql
isolatedloadingproperty_sql
partitionboundspec_sql
partitionedofproperty_sql
lockingproperty_sql
withdataproperty_sql
withsystemversioningproperty_sql
insert_sql
introducer_sql
kill_sql
pseudotype_sql
objectidentifier_sql
onconflict_sql
returning_sql
rowformatdelimitedproperty_sql
withtablehint_sql
indextablehint_sql
historicaldata_sql
table_parts
table_sql
pivot_sql
version_sql
tuple_sql
update_sql
values_sql
var_sql
into_sql
from_sql
groupingsets_sql
rollup_sql
rollupindex_sql
rollupproperty_sql
cube_sql
group_sql
having_sql
connect_sql
prior_sql
lateral_op
lateral_sql
limit_sql
offset_sql
setitem_sql
set_sql
queryband_sql
pragma_sql
lock_sql
literal_sql
escape_str
loaddata_sql
null_sql
boolean_sql
booland_sql
boolor_sql
order_sql
withfill_sql
cluster_sql
clusterproperty_sql
distribute_sql
sort_sql
ordered_sql
matchrecognizemeasure_sql
matchrecognize_sql
query_modifiers
options_modifier
forclause_sql
queryoption_sql
offset_limit_modifiers
after_limit_modifiers
select_sql
schema_sql
schema_columns_sql
star_sql
parameter_sql
sessionparameter_sql
placeholder_sql
subquery_sql
qualify_sql
prewhere_sql
where_sql
partition_by_sql
windowspec_sql
between_sql
bracket_offset_expressions
all_sql
any_sql
exists_sql
case_sql
constraint_sql
nextvaluefor_sql
convert_concat_args
concat_sql
check_sql
foreignkey_sql
primarykey_sql
timeserieskey_sql
if_sql
matchagainst_sql
jsonkeyvalue_sql
jsonpath_sql
json_path_part
formatjson_sql
formatphrase_sql
jsonarray_sql
jsonarrayagg_sql
jsoncolumndef_sql
jsonschema_sql
jsontable_sql
openjsoncolumndef_sql
openjson_sql
in_unnest_op
interval_sql
return_sql
reference_sql
anonymous_sql
paren_sql
neg_sql
not_sql
alias_sql
pivotalias_sql
atindex_sql
attimezone_sql
fromtimezone_sql
fromiso8601date_sql
fromiso8601timestamp_sql
fromiso8601timestampnanos_sql
add_sql
and_sql
or_sql
xor_sql
connector_sql
bitwiseand_sql
bitwiseleftshift_sql
bitwiseor_sql
bitwiserightshift_sql
cast_sql
command_sql
comment_sql
mergetreettlaction_sql
mergetreettl_sql
transaction_sql
commit_sql
rollback_sql
altercolumn_sql
modifycolumn_sql
alterindex_sql
alterdiststyle_sql
altersortkey_sql
alterrename_sql
renamecolumn_sql
alterset_sql
alter_sql
altersession_sql
add_column_sql
droppartition_sql
dropprimarykey_sql
addconstraint_sql
addpartition_sql
distinct_sql
havingmax_sql
intdiv_sql
dpipe_sql
div_sql
safedivide_sql
overlaps_sql
distance_sql
distancend_sql
dot_sql
eq_sql
propertyeq_sql
escape_sql
glob_sql
gt_sql
gte_sql
is_sql
like_sql
ilike_sql
match_sql
similarto_sql
lt_sql
lte_sql
mod_sql
mul_sql
neq_sql
nullsafeeq_sql
nullsafeneq_sql
sub_sql
jsoncast_sql
try_sql
log_sql
use_sql
binary
ceil_floor
function_fallback_sql
func
format_args
too_wide
format_time
expressions
op_expressions
naked_property
tag_sql
token_sql
userdefinedfunction_sql
macrooverloads_sql
macrooverload_sql
joinhint_sql
kwarg_sql
when_sql
whens_sql
merge_sql
tochar_sql
dictproperty_sql
dictrange_sql
dictsubproperty_sql
duplicatekeyproperty_sql
uniquekeyproperty_sql
distributedbyproperty_sql
oncluster_sql
clusteredbyproperty_sql
anyvalue_sql
querytransform_sql
indexconstraintoption_sql
checkcolumnconstraint_sql
indexcolumnconstraint_sql
nvl2_sql
comprehension_sql
columnprefix_sql
opclass_sql
predict_sql
generateembedding_sql
generatetext_sql
generatetable_sql
generatebool_sql
generateint_sql
generatedouble_sql
mltranslate_sql
mlforecast_sql
aiforecast_sql
featuresattime_sql
vectorsearch_sql
forin_sql
refresh_sql
toarray_sql
tsordstotimestamp_sql
tsordstodatetime_sql
tsordstodate_sql
unixdate_sql
lastday_sql
dateadd_sql
arrayany_sql
struct_sql
partitionrange_sql
truncatetable_sql
convert_sql
copyparameter_sql
credentials_sql
copy_sql
semicolon_sql
datadeletionproperty_sql
maskingpolicycolumnconstraint_sql
gapfill_sql
scope_resolution
scoperesolution_sql
changes_sql
summarize_sql
explodinggenerateseries_sql
converttimezone_sql
json_sql
jsonvalue_sql
skipjsoncolumn_sql
conditionalinsert_sql
multitableinserts_sql
oncondition_sql
jsonextractquote_sql
jsonexists_sql
slice_sql
apply_sql
grant_sql
revoke_sql
grantprivilege_sql
grantprincipal_sql
columns_sql
overlay_sql
todouble_sql
string_sql
median_sql
overflowtruncatebehavior_sql
unixseconds_sql
arraysize_sql
attach_sql
detach_sql
attachoption_sql
watermarkcolumnconstraint_sql
encodeproperty_sql
includeproperty_sql
xmlelement_sql
xmlkeyvalueoption_sql
partitionbyrangeproperty_sql
partitionbyrangepropertydynamic_sql
unpivotcolumns_sql
analyzesample_sql
analyzestatistics_sql
analyzehistogram_sql
analyzedelete_sql
analyzelistchainedrows_sql
analyzevalidate_sql
analyze_sql
xmltable_sql
xmlnamespace_sql
export_sql
declare_sql
declareitem_sql
recursivewithsearch_sql
parameterizedagg_sql
anonymousaggfunc_sql
combinedaggfunc_sql
combinedparameterizedagg_sql
get_put_sql
translatecharacters_sql
decodecase_sql
semanticview_sql
getextract_sql
datefromunixdate_sql
buildproperty_sql
refreshtriggerproperty_sql
modelattribute_sql
directorystage_sql
initcap_sql
localtime_sql
localtimestamp_sql
weekstart_name
weekstart_sql
block_sql
functionspecification_sql
storedprocedure_sql
ifblock_sql
casestatement_sql
whileblock_sql
loopblock_sql
repeatblock_sql
leave_sql
iterate_sql
execute_sql
executesql_sql
altermodifysqlsecurity_sql
usingproperty_sql
renameindex_sql