Edit on GitHub

sqlglot.generators.duckdb

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

Transform Snowflake's TIME_SLICE to DuckDB's time_bucket.

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

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

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

Transpile BITMAP_BUCKET_NUMBER function from Snowflake to DuckDB equivalent.

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

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

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

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

Transpile Snowflake's BITMAP_BIT_POSITION to DuckDB CASE expression.

Snowflake's BITMAP_BIT_POSITION behavior:

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

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

Snowflake bitmap format:

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

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

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

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

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

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

TO_BINARY and TRY_TO_BINARY transpilation:

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

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

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

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

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

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:
3052    def timestampfromparts_sql(self, expression: exp.TimestampFromParts) -> str:
3053        # Check if this is the date/time expression form: TIMESTAMP_FROM_PARTS(date_expr, time_expr)
3054        date_expr = expression.this
3055        time_expr = expression.expression
3056
3057        if date_expr is not None and time_expr is not None:
3058            # In DuckDB, DATE + TIME produces TIMESTAMP
3059            return self.sql(exp.Add(this=date_expr, expression=time_expr))
3060
3061        # Component-based form: TIMESTAMP_FROM_PARTS(year, month, day, hour, minute, second, ...)
3062        sec = expression.args.get("sec")
3063        if sec is None:
3064            # This shouldn't happen with valid input, but handle gracefully
3065            return rename_func("MAKE_TIMESTAMP")(self, expression)
3066
3067        milli = expression.args.get("milli")
3068        if milli is not None:
3069            sec += milli.pop() / exp.Literal.number(1000.0)
3070
3071        nano = expression.args.get("nano")
3072        if nano is not None:
3073            sec += nano.pop() / exp.Literal.number(1000000000.0)
3074
3075        if milli or nano:
3076            expression.set("sec", sec)
3077
3078        if zone := expression.args.get("zone"):
3079            return self.sql(exp.AtTimeZone(this=expression, zone=zone.pop()))
3080
3081        return rename_func("MAKE_TIMESTAMP")(self, expression)
@unsupported_args('nano')
def timestampltzfromparts_sql( self, expression: sqlglot.expressions.temporal.TimestampLtzFromParts) -> str:
3083    @unsupported_args("nano")
3084    def timestampltzfromparts_sql(self, expression: exp.TimestampLtzFromParts) -> str:
3085        # Pop nano so rename_func only passes args that MAKE_TIMESTAMP accepts
3086        if nano := expression.args.get("nano"):
3087            nano.pop()
3088
3089        timestamp = rename_func("MAKE_TIMESTAMP")(self, expression)
3090        return f"CAST({timestamp} AS TIMESTAMPTZ)"
@unsupported_args('nano')
def timestamptzfromparts_sql( self, expression: sqlglot.expressions.temporal.TimestampTzFromParts) -> str:
3092    @unsupported_args("nano")
3093    def timestamptzfromparts_sql(self, expression: exp.TimestampTzFromParts) -> str:
3094        # Extract zone before popping
3095        zone = expression.args.get("zone")
3096        # Pop zone and nano so rename_func only passes args that MAKE_TIMESTAMP accepts
3097        if zone:
3098            zone = zone.pop()
3099
3100        if nano := expression.args.get("nano"):
3101            nano.pop()
3102
3103        timestamp = rename_func("MAKE_TIMESTAMP")(self, expression)
3104
3105        if zone:
3106            # Use AT TIME ZONE to apply the explicit timezone
3107            return f"{timestamp} AT TIME ZONE {self.sql(zone)}"
3108
3109        return timestamp
def tablesample_sql( self, expression: sqlglot.expressions.query.TableSample, tablesample_keyword: str | None = None) -> str:
3111    def tablesample_sql(
3112        self,
3113        expression: exp.TableSample,
3114        tablesample_keyword: str | None = None,
3115    ) -> str:
3116        if not isinstance(expression.parent, exp.Select):
3117            # This sample clause only applies to a single source, not the entire resulting relation
3118            tablesample_keyword = "TABLESAMPLE"
3119
3120        if expression.args.get("size"):
3121            method = expression.args.get("method")
3122            if method and method.name.upper() != "RESERVOIR":
3123                self.unsupported(
3124                    f"Sampling method {method} is not supported with a discrete sample count, "
3125                    "defaulting to reservoir sampling"
3126                )
3127                expression.set("method", exp.var("RESERVOIR"))
3128
3129        return super().tablesample_sql(expression, tablesample_keyword=tablesample_keyword)
def in_sql(self, expression: sqlglot.expressions.core.In) -> str:
3131    def in_sql(self, expression: exp.In) -> str:
3132        unnest = expression.args.get("unnest")
3133        if unnest:
3134            return self.sql(
3135                exp.replace_placeholders(
3136                    self.IN_UNNEST_TEMPLATE, arr=unnest.expressions[0], value=expression.this
3137                )
3138            )
3139        return super().in_sql(expression)
def join_sql(self, expression: sqlglot.expressions.query.Join) -> str:
3141    def join_sql(self, expression: exp.Join) -> str:
3142        if (
3143            not expression.args.get("using")
3144            and not expression.args.get("on")
3145            and not expression.method
3146            and (expression.kind in ("", "INNER", "OUTER"))
3147        ):
3148            # Some dialects support `LEFT/INNER JOIN UNNEST(...)` without an explicit ON clause
3149            # DuckDB doesn't, but we can just add a dummy ON clause that is always true
3150            if isinstance(expression.this, exp.Unnest):
3151                return super().join_sql(expression.on(exp.true()))
3152
3153            expression.set("side", None)
3154            expression.set("kind", None)
3155
3156        return super().join_sql(expression)
def countif_sql(self, expression: sqlglot.expressions.aggregate.CountIf) -> str:
3158    def countif_sql(self, expression: exp.CountIf) -> str:
3159        if self.dialect.version >= (1, 2):
3160            this = expression.this
3161            if expression.args.get("zero_on_all_null") and not isinstance(this, exp.Distinct):
3162                # DuckDB >= 1.2's COUNT_IF returns NULL when the condition is NULL on all rows,
3163                # so we wrap the condition in IS TRUE to preserve count-like semantics
3164                expression = exp.CountIf(this=exp.paren(this).is_(exp.true()))
3165            return self.function_fallback_sql(expression)
3166
3167        # https://github.com/tobymao/sqlglot/pull/4749
3168        return count_if_to_sum(self, expression)
def bracket_sql(self, expression: sqlglot.expressions.core.Bracket) -> str:
3170    def bracket_sql(self, expression: exp.Bracket) -> str:
3171        if self.dialect.version >= (1, 2):
3172            return super().bracket_sql(expression)
3173
3174        # https://duckdb.org/2025/02/05/announcing-duckdb-120.html#breaking-changes
3175        this = expression.this
3176        if isinstance(this, exp.Array):
3177            this.replace(exp.paren(this))
3178
3179        bracket = super().bracket_sql(expression)
3180
3181        if not expression.args.get("returns_list_for_maps"):
3182            if not this.type:
3183                from sqlglot.optimizer.annotate_types import annotate_types
3184
3185                this = annotate_types(this, dialect=self.dialect)
3186
3187            if this.is_type(exp.DType.MAP):
3188                bracket = f"({bracket})[1]"
3189
3190        return bracket
def withingroup_sql(self, expression: sqlglot.expressions.core.WithinGroup) -> str:
3192    def withingroup_sql(self, expression: exp.WithinGroup) -> str:
3193        func = expression.this
3194
3195        # For ARRAY_AGG, DuckDB requires ORDER BY inside the function, not in WITHIN GROUP
3196        # Transform: ARRAY_AGG(x) WITHIN GROUP (ORDER BY y) -> ARRAY_AGG(x ORDER BY y)
3197        if isinstance(func, exp.ArrayAgg):
3198            if not isinstance(order := expression.expression, exp.Order):
3199                return self.sql(func)
3200
3201            # Save the original column for FILTER clause (before wrapping with Order)
3202            original_this = func.this
3203
3204            # Move ORDER BY inside ARRAY_AGG by wrapping its argument with Order
3205            # ArrayAgg.this should become Order(this=ArrayAgg.this, expressions=order.expressions)
3206            func.set(
3207                "this",
3208                exp.Order(
3209                    this=func.this.copy(),
3210                    expressions=order.expressions,
3211                ),
3212            )
3213
3214            # Generate the ARRAY_AGG function with ORDER BY and add FILTER clause if needed
3215            # Use original_this (not the Order-wrapped version) for the FILTER condition
3216            array_agg_sql = self.function_fallback_sql(func)
3217            return self._add_arrayagg_null_filter(array_agg_sql, func, original_this)
3218
3219        # For other functions (like PERCENTILES), use existing logic
3220        expression_sql = self.sql(expression, "expression")
3221
3222        if isinstance(func, exp.PERCENTILES):
3223            # Make the order key the first arg and slide the fraction to the right
3224            # https://duckdb.org/docs/sql/aggregates#ordered-set-aggregate-functions
3225            order_col = expression.find(exp.Ordered)
3226            if order_col:
3227                func.set("expression", func.this)
3228                func.set("this", order_col.this)
3229
3230        this = self.sql(expression, "this").rstrip(")")
3231
3232        return f"{this}{expression_sql})"
def length_sql(self, expression: sqlglot.expressions.string.Length) -> str:
3234    def length_sql(self, expression: exp.Length) -> str:
3235        arg = expression.this
3236
3237        # Dialects like BQ and Snowflake also accept binary values as args, so
3238        # DDB will attempt to infer the type or resort to case/when resolution
3239        if not expression.args.get("binary") or arg.is_string:
3240            return self.func("LENGTH", arg)
3241
3242        if not arg.type:
3243            from sqlglot.optimizer.annotate_types import annotate_types
3244
3245            arg = annotate_types(arg, dialect=self.dialect)
3246
3247        if arg.is_type(*exp.DataType.TEXT_TYPES):
3248            return self.func("LENGTH", arg)
3249
3250        # We need these casts to make duckdb's static type checker happy
3251        blob = exp.cast(arg, exp.DType.VARBINARY)
3252        varchar = exp.cast(arg, exp.DType.VARCHAR)
3253
3254        case = (
3255            exp.case(exp.Anonymous(this="TYPEOF", expressions=[arg]))
3256            .when(exp.Literal.string("BLOB"), exp.ByteLength(this=blob))
3257            .else_(exp.Anonymous(this="LENGTH", expressions=[varchar]))
3258        )
3259        return self.sql(case)
def bitlength_sql(self, expression: sqlglot.expressions.string.BitLength) -> str:
3261    def bitlength_sql(self, expression: exp.BitLength) -> str:
3262        if not _is_binary(arg := expression.this):
3263            return self.func("BIT_LENGTH", arg)
3264
3265        blob = exp.cast(arg, exp.DataType.Type.VARBINARY)
3266        return self.sql(exp.ByteLength(this=blob) * exp.Literal.number(8))
def chr_sql( self, expression: sqlglot.expressions.string.Chr, name: str = 'CHR') -> str:
3268    def chr_sql(self, expression: exp.Chr, name: str = "CHR") -> str:
3269        arg = expression.expressions[0]
3270        if arg.is_type(*exp.DataType.REAL_TYPES):
3271            arg = exp.cast(arg, exp.DType.INT)
3272        return self.func("CHR", arg)
def collation_sql(self, expression: sqlglot.expressions.functions.Collation) -> str:
3274    def collation_sql(self, expression: exp.Collation) -> str:
3275        self.unsupported("COLLATION function is not supported by DuckDB")
3276        return self.function_fallback_sql(expression)
def collate_sql(self, expression: sqlglot.expressions.functions.Collate) -> str:
3278    def collate_sql(self, expression: exp.Collate) -> str:
3279        if not expression.expression.is_string:
3280            return super().collate_sql(expression)
3281
3282        raw = expression.expression.name
3283        if not raw:
3284            return self.sql(expression.this)
3285
3286        parts = []
3287        for part in raw.split("-"):
3288            lower = part.lower()
3289            if lower not in _SNOWFLAKE_COLLATION_DEFAULTS:
3290                if lower in _SNOWFLAKE_COLLATION_UNSUPPORTED:
3291                    self.unsupported(
3292                        f"Snowflake collation specifier '{part}' has no DuckDB equivalent"
3293                    )
3294                parts.append(lower)
3295
3296        if not parts:
3297            return self.sql(expression.this)
3298        return super().collate_sql(
3299            exp.Collate(this=expression.this, expression=exp.var(".".join(parts)))
3300        )
def regexpcount_sql(self, expression: sqlglot.expressions.string.RegexpCount) -> str:
3332    def regexpcount_sql(self, expression: exp.RegexpCount) -> str:
3333        this = expression.this
3334        pattern = expression.expression
3335        position = expression.args.get("position")
3336        parameters = expression.args.get("parameters")
3337
3338        # Validate flags - only "ims" flags are supported for embedded patterns
3339        validated_flags = self._validate_regexp_flags(parameters, supported_flags="ims")
3340
3341        if position:
3342            this = exp.Substring(this=this, start=position)
3343
3344        # Embed flags in pattern (REGEXP_EXTRACT_ALL doesn't support flags argument)
3345        if validated_flags:
3346            pattern = exp.Concat(expressions=[exp.Literal.string(f"(?{validated_flags})"), pattern])
3347
3348        # Handle empty pattern: Snowflake returns 0, DuckDB would match between every character
3349        result = (
3350            exp.case()
3351            .when(
3352                exp.EQ(this=pattern, expression=exp.Literal.string("")),
3353                exp.Literal.number(0),
3354            )
3355            .else_(
3356                exp.Length(
3357                    this=exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern])
3358                )
3359            )
3360        )
3361
3362        return self.sql(result)
def regexpreplace_sql(self, expression: sqlglot.expressions.string.RegexpReplace) -> str:
3364    def regexpreplace_sql(self, expression: exp.RegexpReplace) -> str:
3365        subject = expression.this
3366        pattern = expression.expression
3367        replacement = expression.args.get("replacement") or exp.Literal.string("")
3368        position = expression.args.get("position")
3369        occurrence = expression.args.get("occurrence")
3370        modifiers = expression.args.get("modifiers")
3371
3372        validated_flags = self._validate_regexp_flags(modifiers, supported_flags="cimsg") or ""
3373
3374        # Handle occurrence (only literals supported)
3375        if occurrence and not occurrence.is_int:
3376            self.unsupported("REGEXP_REPLACE with non-literal occurrence")
3377        else:
3378            occurrence = occurrence.to_py() if occurrence and occurrence.is_int else 0
3379            if occurrence > 1:
3380                self.unsupported(f"REGEXP_REPLACE occurrence={occurrence} not supported")
3381            # flag duckdb to do either all or none, single_replace check is for duckdb round trip
3382            elif (
3383                occurrence == 0
3384                and "g" not in validated_flags
3385                and not expression.args.get("single_replace")
3386            ):
3387                validated_flags += "g"
3388
3389        # Handle position (only literals supported)
3390        prefix = None
3391        if position and not position.is_int:
3392            self.unsupported("REGEXP_REPLACE with non-literal position")
3393        elif position and position.is_int and position.to_py() > 1:
3394            pos = position.to_py()
3395            prefix = exp.Substring(
3396                this=subject, start=exp.Literal.number(1), length=exp.Literal.number(pos - 1)
3397            )
3398            subject = exp.Substring(this=subject, start=exp.Literal.number(pos))
3399
3400        result: exp.Expr = exp.Anonymous(
3401            this="REGEXP_REPLACE",
3402            expressions=[
3403                subject,
3404                pattern,
3405                replacement,
3406                exp.Literal.string(validated_flags) if validated_flags else None,
3407            ],
3408        )
3409
3410        if prefix:
3411            result = exp.Concat(expressions=[prefix, result])
3412
3413        return self.sql(result)
def regexplike_sql(self, expression: sqlglot.expressions.core.RegexpLike) -> str:
3415    def regexplike_sql(self, expression: exp.RegexpLike) -> str:
3416        this = expression.this
3417        pattern = expression.expression
3418        flag = expression.args.get("flag")
3419
3420        if expression.args.get("full_match"):
3421            validated_flags = self._validate_regexp_flags(flag, supported_flags="cims")
3422            flag = exp.Literal.string(validated_flags) if validated_flags else None
3423            return self.func("REGEXP_FULL_MATCH", this, pattern, flag)
3424
3425        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:
3427    @unsupported_args("ins_cost", "del_cost", "sub_cost")
3428    def levenshtein_sql(self, expression: exp.Levenshtein) -> str:
3429        this = expression.this
3430        expr = expression.expression
3431        max_dist = expression.args.get("max_dist")
3432
3433        if max_dist is None:
3434            return self.func("LEVENSHTEIN", this, expr)
3435
3436        # Emulate Snowflake semantics: if distance > max_dist, return max_dist
3437        levenshtein = exp.Levenshtein(this=this, expression=expr)
3438        return self.sql(exp.Least(this=levenshtein, expressions=[max_dist]))
def pad_sql(self, expression: sqlglot.expressions.string.Pad) -> str:
3440    def pad_sql(self, expression: exp.Pad) -> str:
3441        """
3442        Handle RPAD/LPAD for VARCHAR and BINARY types.
3443
3444        For VARCHAR: Delegate to parent class
3445        For BINARY: Lower to: input || REPEAT(pad, GREATEST(0, target_len - OCTET_LENGTH(input)))
3446        """
3447        string_arg = expression.this
3448        fill_arg = expression.args.get("fill_pattern") or exp.Literal.string(" ")
3449
3450        if _is_binary(string_arg) or _is_binary(fill_arg):
3451            length_arg = expression.expression
3452            is_left = expression.args.get("is_left")
3453
3454            input_len = exp.ByteLength(this=string_arg)
3455            chars_needed = length_arg - input_len
3456            pad_count = exp.Greatest(
3457                this=exp.Literal.number(0), expressions=[chars_needed], ignore_nulls=True
3458            )
3459            repeat_expr = exp.Repeat(this=fill_arg, times=pad_count)
3460
3461            left, right = string_arg, repeat_expr
3462            if is_left:
3463                left, right = right, left
3464
3465            result = exp.DPipe(this=left, expression=right)
3466            return self.sql(result)
3467
3468        # For VARCHAR: Delegate to parent class (handles PAD_FILL_PATTERN_IS_REQUIRED)
3469        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:
3471    def minhash_sql(self, expression: exp.Minhash) -> str:
3472        k = expression.this
3473        exprs = expression.expressions
3474
3475        if len(exprs) != 1 or isinstance(exprs[0], exp.Star):
3476            self.unsupported(
3477                "MINHASH with multiple expressions or * requires manual query restructuring"
3478            )
3479            return self.func("MINHASH", k, *exprs)
3480
3481        expr = exprs[0]
3482        result = exp.replace_placeholders(self.MINHASH_TEMPLATE.copy(), expr=expr, k=k)
3483        return f"({self.sql(result)})"
def minhashcombine_sql(self, expression: sqlglot.expressions.aggregate.MinhashCombine) -> str:
3485    def minhashcombine_sql(self, expression: exp.MinhashCombine) -> str:
3486        expr = expression.this
3487        result = exp.replace_placeholders(self.MINHASH_COMBINE_TEMPLATE.copy(), expr=expr)
3488        return f"({self.sql(result)})"
def approximatesimilarity_sql( self, expression: sqlglot.expressions.aggregate.ApproximateSimilarity) -> str:
3490    def approximatesimilarity_sql(self, expression: exp.ApproximateSimilarity) -> str:
3491        expr = expression.this
3492        result = exp.replace_placeholders(self.APPROXIMATE_SIMILARITY_TEMPLATE.copy(), expr=expr)
3493        return f"({self.sql(result)})"
def arrayuniqueagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayUniqueAgg) -> str:
3495    def arrayuniqueagg_sql(self, expression: exp.ArrayUniqueAgg) -> str:
3496        return self.sql(
3497            exp.Filter(
3498                this=exp.func("LIST", exp.Distinct(expressions=[expression.this])),
3499                expression=exp.Where(this=expression.this.copy().is_(exp.null()).not_()),
3500            )
3501        )
def arrayconcatagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayConcatAgg) -> str:
3503    def arrayconcatagg_sql(self, expression: exp.ArrayConcatAgg) -> str:
3504        this = expression.this
3505
3506        if isinstance(this, exp.Limit):
3507            self.unsupported("LIMIT in ARRAY_CONCAT_AGG cannot be transpiled to DuckDB")
3508            this = this.this
3509
3510        inner = this.this if isinstance(this, exp.Order) else this
3511
3512        return self.func(
3513            "FLATTEN",
3514            exp.Filter(
3515                this=exp.ArrayAgg(this=this),
3516                expression=exp.Where(this=inner.copy().is_(exp.null()).not_()),
3517            ),
3518        )
def arrayunionagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayUnionAgg) -> str:
3520    def arrayunionagg_sql(self, expression: exp.ArrayUnionAgg) -> str:
3521        self.unsupported("ARRAY_UNION_AGG is not supported in DuckDB")
3522        return self.function_fallback_sql(expression)
def arraydistinct_sql(self, expression: sqlglot.expressions.array.ArrayDistinct) -> str:
3524    def arraydistinct_sql(self, expression: exp.ArrayDistinct) -> str:
3525        arr = expression.this
3526        func = self.func("LIST_DISTINCT", arr)
3527
3528        if expression.args.get("check_null"):
3529            add_null_to_array = exp.func(
3530                "LIST_APPEND", exp.func("LIST_DISTINCT", exp.ArrayCompact(this=arr)), exp.Null()
3531            )
3532            return self.sql(
3533                exp.If(
3534                    this=exp.NEQ(
3535                        this=exp.ArraySize(this=arr), expression=exp.func("LIST_COUNT", arr)
3536                    ),
3537                    true=add_null_to_array,
3538                    false=func,
3539                )
3540            )
3541
3542        return func
def arrayintersect_sql(self, expression: sqlglot.expressions.array.ArrayIntersect) -> str:
3544    def arrayintersect_sql(self, expression: exp.ArrayIntersect) -> str:
3545        if expression.args.get("is_multiset") and len(expression.expressions) == 2:
3546            return self._array_bag_sql(
3547                self.ARRAY_INTERSECTION_CONDITION,
3548                expression.expressions[0],
3549                expression.expressions[1],
3550            )
3551        return self.function_fallback_sql(expression)
def arrayexcept_sql(self, expression: sqlglot.expressions.array.ArrayExcept) -> str:
3553    def arrayexcept_sql(self, expression: exp.ArrayExcept) -> str:
3554        arr1, arr2 = expression.this, expression.expression
3555        if expression.args.get("is_multiset"):
3556            return self._array_bag_sql(self.ARRAY_EXCEPT_CONDITION, arr1, arr2)
3557        return self.sql(
3558            exp.replace_placeholders(self.ARRAY_EXCEPT_SET_TEMPLATE, arr1=arr1, arr2=arr2)
3559        )
def arrayslice_sql(self, expression: sqlglot.expressions.array.ArraySlice) -> str:
3561    def arrayslice_sql(self, expression: exp.ArraySlice) -> str:
3562        """
3563        Transpiles Snowflake's ARRAY_SLICE (0-indexed, exclusive end) to DuckDB's
3564        ARRAY_SLICE (1-indexed, inclusive end) by wrapping start and end in CASE
3565        expressions that adjust the index at query time:
3566          - start: CASE WHEN start >= 0 THEN start + 1 ELSE start END
3567          - end:   CASE WHEN end < 0 THEN end - 1 ELSE end END
3568        """
3569        start, end = expression.args.get("start"), expression.args.get("end")
3570
3571        if expression.args.get("zero_based"):
3572            if start is not None:
3573                start = (
3574                    exp.case()
3575                    .when(
3576                        exp.GTE(this=start.copy(), expression=exp.Literal.number(0)),
3577                        exp.Add(this=start.copy(), expression=exp.Literal.number(1)),
3578                    )
3579                    .else_(start)
3580                )
3581            if end is not None:
3582                end = (
3583                    exp.case()
3584                    .when(
3585                        exp.LT(this=end.copy(), expression=exp.Literal.number(0)),
3586                        exp.Sub(this=end.copy(), expression=exp.Literal.number(1)),
3587                    )
3588                    .else_(end)
3589                )
3590
3591        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:
3593    def arrayszip_sql(self, expression: exp.ArraysZip) -> str:
3594        args = expression.expressions
3595
3596        if not args:
3597            # Return [{}] - using MAP([], []) since DuckDB can't represent empty structs
3598            return self.sql(exp.array(exp.Map(keys=exp.array(), values=exp.array())))
3599
3600        # Build placeholder values for template
3601        lengths = [exp.Length(this=arg) for arg in args]
3602        max_len = (
3603            lengths[0]
3604            if len(lengths) == 1
3605            else exp.Greatest(this=lengths[0], expressions=lengths[1:])
3606        )
3607
3608        # Empty struct with same schema: {'$1': NULL, '$2': NULL, ...}
3609        empty_struct = exp.func(
3610            "STRUCT",
3611            *[
3612                exp.PropertyEQ(this=exp.Literal.string(f"${i + 1}"), expression=exp.Null())
3613                for i in range(len(args))
3614            ],
3615        )
3616
3617        # Struct for transform: {'$1': COALESCE(arr1, [])[__i + 1], ...}
3618        # COALESCE wrapping handles NULL arrays - prevents invalid NULL[i] syntax
3619        index = exp.column("__i") + 1
3620        transform_struct = exp.func(
3621            "STRUCT",
3622            *[
3623                exp.PropertyEQ(
3624                    this=exp.Literal.string(f"${i + 1}"),
3625                    expression=exp.func("COALESCE", arg, exp.array())[index],
3626                )
3627                for i, arg in enumerate(args)
3628            ],
3629        )
3630
3631        result = exp.replace_placeholders(
3632            self.ARRAYS_ZIP_TEMPLATE.copy(),
3633            null_check=exp.or_(*[arg.is_(exp.Null()) for arg in args]),
3634            all_empty_check=exp.and_(
3635                *[
3636                    exp.EQ(this=exp.Length(this=arg), expression=exp.Literal.number(0))
3637                    for arg in args
3638                ]
3639            ),
3640            empty_struct=empty_struct,
3641            max_len=max_len,
3642            transform_struct=transform_struct,
3643        )
3644        return self.sql(result)
def lower_sql(self, expression: sqlglot.expressions.string.Lower) -> str:
3646    def lower_sql(self, expression: exp.Lower) -> str:
3647        result_sql = self.func("LOWER", _cast_to_varchar(expression.this))
3648        return _gen_with_cast_to_blob(self, expression, result_sql)
def upper_sql(self, expression: sqlglot.expressions.string.Upper) -> str:
3650    def upper_sql(self, expression: exp.Upper) -> str:
3651        result_sql = self.func("UPPER", _cast_to_varchar(expression.this))
3652        return _gen_with_cast_to_blob(self, expression, result_sql)
def reverse_sql(self, expression: sqlglot.expressions.string.Reverse) -> str:
3654    def reverse_sql(self, expression: exp.Reverse) -> str:
3655        result_sql = self.func("REVERSE", _cast_to_varchar(expression.this))
3656        return _gen_with_cast_to_blob(self, expression, result_sql)
def left_sql(self, expression: sqlglot.expressions.string.Left) -> str:
3682    def left_sql(self, expression: exp.Left) -> str:
3683        return self._left_right_sql(expression, "LEFT")
def right_sql(self, expression: sqlglot.expressions.string.Right) -> str:
3685    def right_sql(self, expression: exp.Right) -> str:
3686        return self._left_right_sql(expression, "RIGHT")
def rtrimmedlength_sql(self, expression: sqlglot.expressions.string.RtrimmedLength) -> str:
3688    def rtrimmedlength_sql(self, expression: exp.RtrimmedLength) -> str:
3689        return self.func("LENGTH", exp.Trim(this=expression.this, position="TRAILING"))
def stuff_sql(self, expression: sqlglot.expressions.string.Stuff) -> str:
3691    def stuff_sql(self, expression: exp.Stuff) -> str:
3692        base = expression.this
3693        start = expression.args["start"]
3694        length = expression.args["length"]
3695        insertion = expression.expression
3696        is_binary = _is_binary(base)
3697
3698        if is_binary:
3699            # DuckDB's SUBSTRING doesn't accept BLOB; operate on the HEX string instead
3700            # (each byte = 2 hex chars), then UNHEX back to BLOB
3701            base = exp.Hex(this=base)
3702            insertion = exp.Hex(this=insertion)
3703            left = exp.Substring(
3704                this=base.copy(),
3705                start=exp.Literal.number(1),
3706                length=(start.copy() - exp.Literal.number(1)) * exp.Literal.number(2),
3707            )
3708            right = exp.Substring(
3709                this=base.copy(),
3710                start=((start + length) - exp.Literal.number(1)) * exp.Literal.number(2)
3711                + exp.Literal.number(1),
3712            )
3713        else:
3714            left = exp.Substring(
3715                this=base.copy(),
3716                start=exp.Literal.number(1),
3717                length=start.copy() - exp.Literal.number(1),
3718            )
3719            right = exp.Substring(this=base.copy(), start=start + length)
3720        result: exp.Expr = exp.DPipe(
3721            this=exp.DPipe(this=left, expression=insertion), expression=right
3722        )
3723
3724        if is_binary:
3725            result = exp.Unhex(this=result)
3726
3727        return self.sql(result)
def rand_sql(self, expression: sqlglot.expressions.functions.Rand) -> str:
3729    def rand_sql(self, expression: exp.Rand) -> str:
3730        seed = expression.this
3731        if seed is not None:
3732            self.unsupported("RANDOM with seed is not supported in DuckDB")
3733
3734        lower = expression.args.get("lower")
3735        upper = expression.args.get("upper")
3736
3737        if lower and upper:
3738            # scale DuckDB's [0,1) to the specified range
3739            range_size = exp.paren(upper - lower)
3740            scaled = exp.Add(this=lower, expression=exp.func("random") * range_size)
3741
3742            # For now we assume that if bounds are set, return type is BIGINT. Snowflake/Teradata
3743            result = exp.cast(scaled, exp.DType.BIGINT)
3744            return self.sql(result)
3745
3746        # Default DuckDB behavior - just return RANDOM() as float
3747        return "RANDOM()"
def bytelength_sql(self, expression: sqlglot.expressions.string.ByteLength) -> str:
3749    def bytelength_sql(self, expression: exp.ByteLength) -> str:
3750        arg = expression.this
3751
3752        # Check if it's a text type (handles both literals and annotated expressions)
3753        if arg.is_type(*exp.DataType.TEXT_TYPES):
3754            return self.func("OCTET_LENGTH", exp.Encode(this=arg))
3755
3756        # Default: pass through as-is (conservative for DuckDB, handles binary and unannotated)
3757        return self.func("OCTET_LENGTH", arg)
def base64encode_sql(self, expression: sqlglot.expressions.string.Base64Encode) -> str:
3759    def base64encode_sql(self, expression: exp.Base64Encode) -> str:
3760        # DuckDB TO_BASE64 requires BLOB input
3761        # Snowflake BASE64_ENCODE accepts both VARCHAR and BINARY - for VARCHAR it implicitly
3762        # encodes UTF-8 bytes. We add ENCODE unless the input is a binary type.
3763        result = expression.this
3764
3765        # Check if input is a string type - ENCODE only accepts VARCHAR
3766        if result.is_type(*exp.DataType.TEXT_TYPES):
3767            result = exp.Encode(this=result)
3768
3769        result = exp.ToBase64(this=result)
3770
3771        max_line_length = expression.args.get("max_line_length")
3772        alphabet = expression.args.get("alphabet")
3773
3774        # Handle custom alphabet by replacing standard chars with custom ones
3775        result = _apply_base64_alphabet_replacements(result, alphabet)
3776
3777        # Handle max_line_length by inserting newlines every N characters
3778        line_length = (
3779            t.cast(int, max_line_length.to_py())
3780            if isinstance(max_line_length, exp.Literal) and max_line_length.is_number
3781            else 0
3782        )
3783        if line_length > 0:
3784            newline = exp.Chr(expressions=[exp.Literal.number(10)])
3785            result = exp.Trim(
3786                this=exp.RegexpReplace(
3787                    this=result,
3788                    expression=exp.Literal.string(f"(.{{{line_length}}})"),
3789                    replacement=exp.Concat(expressions=[exp.Literal.string("\\1"), newline.copy()]),
3790                ),
3791                expression=newline,
3792                position="TRAILING",
3793            )
3794
3795        return self.sql(result)
def hex_sql(self, expression: sqlglot.expressions.string.Hex) -> str:
3797    def hex_sql(self, expression: exp.Hex) -> str:
3798        case = expression.args.get("case")
3799
3800        if not case:
3801            return self.func("HEX", expression.this)
3802
3803        hex_expr = exp.Hex(this=expression.this)
3804        return self.sql(
3805            exp.case()
3806            .when(case.is_(exp.null()), exp.null())
3807            .when(case.copy().eq(0), exp.Lower(this=hex_expr.copy()))
3808            .else_(hex_expr)
3809        )
def replace_sql(self, expression: sqlglot.expressions.string.Replace) -> str:
3811    def replace_sql(self, expression: exp.Replace) -> str:
3812        result_sql = self.func(
3813            "REPLACE",
3814            _cast_to_varchar(expression.this),
3815            _cast_to_varchar(expression.expression),
3816            _cast_to_varchar(expression.args.get("replacement")),
3817        )
3818        return _gen_with_cast_to_blob(self, expression, result_sql)
def bitwisexor_sql(self, expression: sqlglot.expressions.core.BitwiseXor) -> str:
3825    def bitwisexor_sql(self, expression: exp.BitwiseXor) -> str:
3826        _prepare_binary_bitwise_args(expression)
3827        result_sql = self.func("XOR", expression.this, expression.expression)
3828        return _gen_with_cast_to_blob(self, expression, result_sql)
def objectinsert_sql(self, expression: sqlglot.expressions.json.ObjectInsert) -> str:
3830    def objectinsert_sql(self, expression: exp.ObjectInsert) -> str:
3831        this = expression.this
3832        key = expression.args.get("key")
3833        key_sql = key.name if isinstance(key, exp.Expr) else ""
3834        value_sql = self.sql(expression, "value")
3835
3836        kv_sql = f"{key_sql} := {value_sql}"
3837
3838        # If the input struct is empty e.g. transpiling OBJECT_INSERT(OBJECT_CONSTRUCT(), key, value) from Snowflake
3839        # then we can generate STRUCT_PACK which will build it since STRUCT_INSERT({}, key := value) is not valid DuckDB
3840        if isinstance(this, exp.Struct) and not this.expressions:
3841            return self.func("STRUCT_PACK", kv_sql)
3842
3843        return self.func("STRUCT_INSERT", this, kv_sql)
def mapcat_sql(self, expression: sqlglot.expressions.array.MapCat) -> str:
3845    def mapcat_sql(self, expression: exp.MapCat) -> str:
3846        result = exp.replace_placeholders(
3847            self.MAPCAT_TEMPLATE.copy(),
3848            map1=expression.this,
3849            map2=expression.expression,
3850        )
3851        return self.sql(result)
def mapcontainskey_sql(self, expression: sqlglot.expressions.array.MapContainsKey) -> str:
3853    def mapcontainskey_sql(self, expression: exp.MapContainsKey) -> str:
3854        return self.func(
3855            "ARRAY_CONTAINS", exp.func("MAP_KEYS", expression.args["key"]), expression.this
3856        )
def mapdelete_sql(self, expression: sqlglot.expressions.array.MapDelete) -> str:
3858    def mapdelete_sql(self, expression: exp.MapDelete) -> str:
3859        map_arg = expression.this
3860        keys_to_delete = expression.expressions
3861
3862        x_dot_key = exp.Dot(this=exp.to_identifier("x"), expression=exp.to_identifier("key"))
3863
3864        lambda_expr = exp.Lambda(
3865            this=exp.In(this=x_dot_key, expressions=keys_to_delete).not_(),
3866            expressions=[exp.to_identifier("x")],
3867        )
3868        result = exp.func(
3869            "MAP_FROM_ENTRIES",
3870            exp.ArrayFilter(this=exp.func("MAP_ENTRIES", map_arg), expression=lambda_expr),
3871        )
3872        return self.sql(result)
def mappick_sql(self, expression: sqlglot.expressions.array.MapPick) -> str:
3874    def mappick_sql(self, expression: exp.MapPick) -> str:
3875        map_arg = expression.this
3876        keys_to_pick = expression.expressions
3877
3878        x_dot_key = exp.Dot(this=exp.to_identifier("x"), expression=exp.to_identifier("key"))
3879
3880        if len(keys_to_pick) == 1 and keys_to_pick[0].is_type(exp.DType.ARRAY):
3881            lambda_expr = exp.Lambda(
3882                this=exp.func("ARRAY_CONTAINS", keys_to_pick[0], x_dot_key),
3883                expressions=[exp.to_identifier("x")],
3884            )
3885        else:
3886            lambda_expr = exp.Lambda(
3887                this=exp.In(this=x_dot_key, expressions=keys_to_pick),
3888                expressions=[exp.to_identifier("x")],
3889            )
3890
3891        result = exp.func(
3892            "MAP_FROM_ENTRIES",
3893            exp.func("LIST_FILTER", exp.func("MAP_ENTRIES", map_arg), lambda_expr),
3894        )
3895        return self.sql(result)
def mapsize_sql(self, expression: sqlglot.expressions.array.MapSize) -> str:
3897    def mapsize_sql(self, expression: exp.MapSize) -> str:
3898        return self.func("CARDINALITY", expression.this)
@unsupported_args('update_flag')
def mapinsert_sql(self, expression: sqlglot.expressions.array.MapInsert) -> str:
3900    @unsupported_args("update_flag")
3901    def mapinsert_sql(self, expression: exp.MapInsert) -> str:
3902        map_arg = expression.this
3903        key = expression.args.get("key")
3904        value = expression.args.get("value")
3905
3906        map_type = map_arg.type
3907
3908        if value is not None:
3909            if map_type and map_type.expressions and len(map_type.expressions) > 1:
3910                # Extract the value type from MAP(key_type, value_type)
3911                value_type = map_type.expressions[1]
3912                # Cast value to match the map's value type to avoid type conflicts
3913                value = exp.cast(value, value_type)
3914            # else: polymorphic MAP case - no type parameters available, use value as-is
3915
3916        # Create a single-entry map for the new key-value pair
3917        new_entry_struct = exp.Struct(expressions=[exp.PropertyEQ(this=key, expression=value)])
3918        new_entry: exp.Expression = exp.ToMap(this=new_entry_struct)
3919
3920        # Use MAP_CONCAT to merge the original map with the new entry
3921        # This automatically handles both insert and update cases
3922        result = exp.func("MAP_CONCAT", map_arg, new_entry)
3923
3924        return self.sql(result)
def startswith_sql(self, expression: sqlglot.expressions.string.StartsWith) -> str:
3926    def startswith_sql(self, expression: exp.StartsWith) -> str:
3927        return self.func(
3928            "STARTS_WITH",
3929            _cast_to_varchar(expression.this),
3930            _cast_to_varchar(expression.expression),
3931        )
def space_sql(self, expression: sqlglot.expressions.string.Space) -> str:
3933    def space_sql(self, expression: exp.Space) -> str:
3934        # DuckDB's REPEAT requires BIGINT for the count parameter
3935        return self.sql(
3936            exp.Repeat(
3937                this=exp.Literal.string(" "),
3938                times=exp.cast(expression.this, exp.DType.BIGINT),
3939            )
3940        )
def tablefromrows_sql(self, expression: sqlglot.expressions.query.TableFromRows) -> str:
3942    def tablefromrows_sql(self, expression: exp.TableFromRows) -> str:
3943        # For GENERATOR, unwrap TABLE() - just emit the Generator (becomes RANGE)
3944        if isinstance(expression.this, exp.Generator):
3945            # Preserve alias, joins, and other table-level args
3946            table = exp.Table(
3947                this=expression.this,
3948                alias=expression.args.get("alias"),
3949                joins=expression.args.get("joins"),
3950            )
3951            return self.sql(table)
3952
3953        return super().tablefromrows_sql(expression)
def unnest_sql(self, expression: sqlglot.expressions.array.Unnest) -> str:
3955    def unnest_sql(self, expression: exp.Unnest) -> str:
3956        explode_array = expression.args.get("explode_array")
3957        if explode_array:
3958            # In BigQuery, UNNESTing a nested array leads to explosion of the top-level array & struct
3959            # This is transpiled to DDB by transforming "FROM UNNEST(...)" to "FROM (SELECT UNNEST(..., max_depth => 2))"
3960            expression.expressions.append(
3961                exp.Kwarg(this=exp.var("max_depth"), expression=exp.Literal.number(2))
3962            )
3963
3964            # If BQ's UNNEST is aliased, we transform it from a column alias to a table alias in DDB
3965            alias = expression.args.get("alias")
3966            if isinstance(alias, exp.TableAlias):
3967                expression.set("alias", None)
3968                if alias.columns:
3969                    alias = exp.TableAlias(this=seq_get(alias.columns, 0))
3970
3971            unnest_sql = super().unnest_sql(expression)
3972            select = exp.Select(expressions=[unnest_sql]).subquery(alias)
3973            return self.sql(select)
3974
3975        return super().unnest_sql(expression)
def arrayagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayAgg) -> str:
3977    def arrayagg_sql(self, expression: exp.ArrayAgg) -> str:
3978        if isinstance(expression.this, exp.Limit):
3979            self.unsupported("LIMIT inside ARRAY_AGG is not supported in DuckDB")
3980
3981        return super().arrayagg_sql(expression)
def ignorenulls_sql(self, expression: sqlglot.expressions.core.IgnoreNulls) -> str:
3983    def ignorenulls_sql(self, expression: exp.IgnoreNulls) -> str:
3984        this = expression.this
3985
3986        if isinstance(this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
3987            # DuckDB should render IGNORE NULLS only for the general-purpose
3988            # window functions that accept it e.g. FIRST_VALUE(... IGNORE NULLS) OVER (...)
3989            return super().ignorenulls_sql(expression)
3990
3991        # For ARRAY_AGG(expr IGNORE NULLS ...), convert IGNORE NULLS to a
3992        # FILTER(WHERE expr IS NOT NULL) clause by setting nulls_excluded on
3993        # the ArrayAgg.  The existing _add_arrayagg_null_filter method will
3994        # emit the FILTER clause during arrayagg_sql / withingroup_sql.
3995        if isinstance(this, exp.ArrayAgg):
3996            this.set("nulls_excluded", True)
3997            return self.sql(this)
3998
3999        if isinstance(this, exp.First):
4000            this = exp.AnyValue(this=this.this)
4001
4002        if not isinstance(this, (exp.AnyValue, exp.ApproxQuantiles)):
4003            self.unsupported("IGNORE NULLS is not supported for non-window functions.")
4004
4005        return self.sql(this)
def split_sql(self, expression: sqlglot.expressions.string.Split) -> str:
4007    def split_sql(self, expression: exp.Split) -> str:
4008        base_func = exp.func("STR_SPLIT", expression.this, expression.expression)
4009
4010        case_expr = exp.case().else_(base_func)
4011        needs_case = False
4012
4013        if expression.args.get("null_returns_null"):
4014            case_expr = case_expr.when(expression.expression.is_(exp.null()), exp.null())
4015            needs_case = True
4016
4017        if expression.args.get("empty_delimiter_returns_whole"):
4018            # When delimiter is empty string, return input string as single array element
4019            array_with_input = exp.array(expression.this)
4020            case_expr = case_expr.when(
4021                expression.expression.eq(exp.Literal.string("")), array_with_input
4022            )
4023            needs_case = True
4024
4025        return self.sql(case_expr if needs_case else base_func)
def splitpart_sql(self, expression: sqlglot.expressions.string.SplitPart) -> str:
4027    def splitpart_sql(self, expression: exp.SplitPart) -> str:
4028        string_arg = expression.this
4029        delimiter_arg = expression.args.get("delimiter")
4030        part_index_arg = expression.args.get("part_index")
4031
4032        if delimiter_arg and part_index_arg:
4033            # Handle Snowflake's "index 0 and 1 both return first element" behavior
4034            if expression.args.get("part_index_zero_as_one"):
4035                # Convert 0 to 1 for compatibility
4036
4037                part_index_arg = exp.Paren(
4038                    this=exp.case()
4039                    .when(part_index_arg.eq(exp.Literal.number("0")), exp.Literal.number("1"))
4040                    .else_(part_index_arg)
4041                )
4042
4043            # Use Anonymous to avoid recursion
4044            base_func_expr: exp.Expr = exp.Anonymous(
4045                this="SPLIT_PART", expressions=[string_arg, delimiter_arg, part_index_arg]
4046            )
4047            needs_case_transform = False
4048            case_expr = exp.case().else_(base_func_expr)
4049
4050            if expression.args.get("empty_delimiter_returns_whole"):
4051                # When delimiter is empty string:
4052                # - Return whole string if part_index is 1 or -1
4053                # - Return empty string otherwise
4054                empty_case = exp.Paren(
4055                    this=exp.case()
4056                    .when(
4057                        exp.or_(
4058                            part_index_arg.eq(exp.Literal.number("1")),
4059                            part_index_arg.eq(exp.Literal.number("-1")),
4060                        ),
4061                        string_arg,
4062                    )
4063                    .else_(exp.Literal.string(""))
4064                )
4065
4066                case_expr = case_expr.when(delimiter_arg.eq(exp.Literal.string("")), empty_case)
4067                needs_case_transform = True
4068
4069            """
4070            Output looks something like this:
4071
4072            CASE
4073            WHEN delimiter is '' THEN
4074                (
4075                    CASE
4076                    WHEN adjusted_part_index = 1 OR adjusted_part_index = -1 THEN input
4077                    ELSE '' END
4078                )
4079            ELSE SPLIT_PART(input, delimiter, adjusted_part_index)
4080            END
4081
4082            """
4083            return self.sql(case_expr if needs_case_transform else base_func_expr)
4084
4085        return self.function_fallback_sql(expression)
def respectnulls_sql(self, expression: sqlglot.expressions.core.RespectNulls) -> str:
4087    def respectnulls_sql(self, expression: exp.RespectNulls) -> str:
4088        if isinstance(expression.this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
4089            # DuckDB should render RESPECT NULLS only for the general-purpose
4090            # window functions that accept it e.g. FIRST_VALUE(... RESPECT NULLS) OVER (...)
4091            return super().respectnulls_sql(expression)
4092
4093        self.unsupported("RESPECT NULLS is not supported for non-window functions.")
4094        return self.sql(expression, "this")
def arraytostring_sql(self, expression: sqlglot.expressions.array.ArrayToString) -> str:
4096    def arraytostring_sql(self, expression: exp.ArrayToString) -> str:
4097        null = expression.args.get("null")
4098
4099        if expression.args.get("null_is_empty"):
4100            x = exp.to_identifier("x")
4101            list_transform = exp.Transform(
4102                this=expression.this.copy(),
4103                expression=exp.Lambda(
4104                    this=exp.Coalesce(
4105                        this=exp.cast(x, "TEXT"), expressions=[exp.Literal.string("")]
4106                    ),
4107                    expressions=[x],
4108                ),
4109            )
4110            array_to_string = exp.ArrayToString(
4111                this=list_transform, expression=expression.expression
4112            )
4113            if expression.args.get("null_delim_is_null"):
4114                return self.sql(
4115                    exp.case()
4116                    .when(expression.expression.copy().is_(exp.null()), exp.null())
4117                    .else_(array_to_string)
4118                )
4119            return self.sql(array_to_string)
4120
4121        if null:
4122            x = exp.to_identifier("x")
4123            return self.sql(
4124                exp.ArrayToString(
4125                    this=exp.Transform(
4126                        this=expression.this,
4127                        expression=exp.Lambda(
4128                            this=exp.Coalesce(this=x, expressions=[null]),
4129                            expressions=[x],
4130                        ),
4131                    ),
4132                    expression=expression.expression,
4133                )
4134            )
4135
4136        return self.func("ARRAY_TO_STRING", expression.this, expression.expression)
def concatws_sql(self, expression: sqlglot.expressions.string.ConcatWs) -> str:
4138    def concatws_sql(self, expression: exp.ConcatWs) -> str:
4139        # DuckDB-specific: handle binary types using DPipe (||) operator
4140        separator = seq_get(expression.expressions, 0)
4141        args = expression.expressions[1:]
4142
4143        if any(_is_binary(arg) for arg in [separator, *args]):
4144            result = args[0]
4145            for arg in args[1:]:
4146                result = exp.DPipe(
4147                    this=exp.DPipe(this=result, expression=separator), expression=arg
4148                )
4149            return self.sql(result)
4150
4151        return super().concatws_sql(expression)
def regexpextract_sql(self, expression: sqlglot.expressions.string.RegexpExtract) -> str:
4206    def regexpextract_sql(self, expression: exp.RegexpExtract) -> str:
4207        return self._regexp_extract_sql(expression)
def regexpextractall_sql(self, expression: sqlglot.expressions.string.RegexpExtractAll) -> str:
4209    def regexpextractall_sql(self, expression: exp.RegexpExtractAll) -> str:
4210        return self._regexp_extract_sql(expression)
def regexpinstr_sql(self, expression: sqlglot.expressions.string.RegexpInstr) -> str:
4212    def regexpinstr_sql(self, expression: exp.RegexpInstr) -> str:
4213        this = expression.this
4214        pattern = expression.expression
4215        position = expression.args.get("position")
4216        orig_occ = expression.args.get("occurrence")
4217        occurrence = orig_occ or exp.Literal.number(1)
4218        option = expression.args.get("option")
4219        parameters = expression.args.get("parameters")
4220
4221        validated_flags = self._validate_regexp_flags(parameters, supported_flags="ims")
4222        if validated_flags:
4223            pattern = exp.Concat(expressions=[exp.Literal.string(f"(?{validated_flags})"), pattern])
4224
4225        # Handle starting position offset
4226        pos_offset: exp.Expr = exp.Literal.number(0)
4227        if position and (not position.is_int or position.to_py() > 1):
4228            this = exp.Substring(this=this, start=position)
4229            pos_offset = position - exp.Literal.number(1)
4230
4231        # Helper: LIST_SUM(LIST_TRANSFORM(list[1:end], x -> LENGTH(x)))
4232        def sum_lengths(func_name: str, end: exp.Expr) -> exp.Expr:
4233            lst = exp.Bracket(
4234                this=exp.Anonymous(this=func_name, expressions=[this, pattern]),
4235                expressions=[exp.Slice(this=exp.Literal.number(1), expression=end)],
4236                offset=1,
4237            )
4238            transform = exp.Anonymous(
4239                this="LIST_TRANSFORM",
4240                expressions=[
4241                    lst,
4242                    exp.Lambda(
4243                        this=exp.Length(this=exp.to_identifier("x")),
4244                        expressions=[exp.to_identifier("x")],
4245                    ),
4246                ],
4247            )
4248            return exp.Coalesce(
4249                this=exp.Anonymous(this="LIST_SUM", expressions=[transform]),
4250                expressions=[exp.Literal.number(0)],
4251            )
4252
4253        # Position = 1 + sum(split_lengths[1:occ]) + sum(match_lengths[1:occ-1]) + offset
4254        base_pos: exp.Expr = (
4255            exp.Literal.number(1)
4256            + sum_lengths("STRING_SPLIT_REGEX", occurrence)
4257            + sum_lengths("REGEXP_EXTRACT_ALL", occurrence - exp.Literal.number(1))
4258            + pos_offset
4259        )
4260
4261        # option=1: add match length for end position
4262        if option and option.is_int and option.to_py() == 1:
4263            match_at_occ = exp.Bracket(
4264                this=exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern]),
4265                expressions=[occurrence],
4266                offset=1,
4267            )
4268            base_pos = base_pos + exp.Coalesce(
4269                this=exp.Length(this=match_at_occ), expressions=[exp.Literal.number(0)]
4270            )
4271
4272        # NULL checks for all provided arguments
4273        # .copy() is used strictly because .is_() alters the node's parent pointer, mutating the parsed AST
4274        null_args = [
4275            expression.this,
4276            expression.expression,
4277            position,
4278            orig_occ,
4279            option,
4280            parameters,
4281        ]
4282        null_checks = [arg.copy().is_(exp.Null()) for arg in null_args if arg]
4283
4284        matches = exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern])
4285
4286        return self.sql(
4287            exp.case()
4288            .when(exp.or_(*null_checks), exp.Null())
4289            .when(pattern.copy().eq(exp.Literal.string("")), exp.Literal.number(0))
4290            .when(exp.Length(this=matches) < occurrence, exp.Literal.number(0))
4291            .else_(base_pos)
4292        )
@unsupported_args('culture')
def numbertostr_sql(self, expression: sqlglot.expressions.string.NumberToStr) -> str:
4294    @unsupported_args("culture")
4295    def numbertostr_sql(self, expression: exp.NumberToStr) -> str:
4296        fmt = expression.args.get("format")
4297        if fmt and fmt.is_int:
4298            return self.func("FORMAT", f"'{{:,.{fmt.name}f}}'", expression.this)
4299
4300        self.unsupported("Only integer formats are supported by NumberToStr")
4301        return self.function_fallback_sql(expression)
def autoincrementcolumnconstraint_sql(self, _) -> str:
4303    def autoincrementcolumnconstraint_sql(self, _) -> str:
4304        self.unsupported("The AUTOINCREMENT column constraint is not supported by DuckDB")
4305        return ""
def aliases_sql(self, expression: sqlglot.expressions.core.Aliases) -> str:
4307    def aliases_sql(self, expression: exp.Aliases) -> str:
4308        this = expression.this
4309        if isinstance(this, exp.Posexplode):
4310            return self.posexplode_sql(this)
4311
4312        return super().aliases_sql(expression)
def posexplode_sql(self, expression: sqlglot.expressions.array.Posexplode) -> str:
4314    def posexplode_sql(self, expression: exp.Posexplode) -> str:
4315        this = expression.this
4316        parent = expression.parent
4317
4318        # The default Spark aliases are "pos" and "col", unless specified otherwise
4319        pos, col = exp.to_identifier("pos"), exp.to_identifier("col")
4320
4321        if isinstance(parent, exp.Aliases):
4322            # Column case: SELECT POSEXPLODE(col) [AS (a, b)]
4323            pos, col = parent.expressions
4324        elif isinstance(parent, exp.Table):
4325            # Table case: SELECT * FROM POSEXPLODE(col) [AS (a, b)]
4326            alias = parent.args.get("alias")
4327            if alias:
4328                pos, col = alias.columns or [pos, col]
4329                alias.pop()
4330
4331        # Translate POSEXPLODE to UNNEST + GENERATE_SUBSCRIPTS
4332        # Note: In Spark pos is 0-indexed, but in DuckDB it's 1-indexed, so we subtract 1 from GENERATE_SUBSCRIPTS
4333        unnest_sql = self.sql(exp.Unnest(expressions=[this], alias=col))
4334        gen_subscripts = self.sql(
4335            exp.Alias(
4336                this=exp.Anonymous(
4337                    this="GENERATE_SUBSCRIPTS", expressions=[this, exp.Literal.number(1)]
4338                )
4339                - exp.Literal.number(1),
4340                alias=pos,
4341            )
4342        )
4343
4344        posexplode_sql = self.format_args(gen_subscripts, unnest_sql)
4345
4346        if isinstance(parent, exp.From) or (parent and isinstance(parent.parent, exp.From)):
4347            # SELECT * FROM POSEXPLODE(col) -> SELECT * FROM (SELECT GENERATE_SUBSCRIPTS(...), UNNEST(...))
4348            return self.sql(exp.Subquery(this=exp.Select(expressions=[posexplode_sql])))
4349
4350        return posexplode_sql
def addmonths_sql(self, expression: sqlglot.expressions.temporal.AddMonths) -> str:
4352    def addmonths_sql(self, expression: exp.AddMonths) -> str:
4353        """
4354        Handles three key issues:
4355        1. Float/decimal months: e.g., Snowflake rounds, whereas DuckDB INTERVAL requires integers
4356        2. End-of-month preservation: If input is last day of month, result is last day of result month
4357        3. Type preservation: Maintains DATE/TIMESTAMPTZ types (DuckDB defaults to TIMESTAMP)
4358        """
4359        from sqlglot.optimizer.annotate_types import annotate_types
4360
4361        this = expression.this
4362        if not this.type:
4363            this = annotate_types(this, dialect=self.dialect)
4364
4365        if this.is_type(*exp.DataType.TEXT_TYPES):
4366            this = exp.Cast(this=this, to=exp.DataType(this=exp.DType.TIMESTAMP))
4367
4368        # Detect float/decimal months to apply rounding (Snowflake behavior)
4369        # DuckDB INTERVAL syntax doesn't support non-integer expressions, so use TO_MONTHS
4370        months_expr = expression.expression
4371        if not months_expr.type:
4372            months_expr = annotate_types(months_expr, dialect=self.dialect)
4373
4374        # Build interval or to_months expression based on type
4375        # Float/decimal case: Round and use TO_MONTHS(CAST(ROUND(value) AS INT))
4376        interval_or_to_months = (
4377            exp.func("TO_MONTHS", exp.cast(exp.func("ROUND", months_expr), "INT"))
4378            if months_expr.is_type(
4379                exp.DType.FLOAT,
4380                exp.DType.DOUBLE,
4381                exp.DType.DECIMAL,
4382            )
4383            # Integer case: standard INTERVAL N MONTH syntax
4384            else exp.Interval(this=months_expr, unit=exp.var("MONTH"))
4385        )
4386
4387        date_add_expr = exp.Add(this=this, expression=interval_or_to_months)
4388
4389        # Apply end-of-month preservation if Snowflake flag is set
4390        # CASE WHEN LAST_DAY(date) = date THEN LAST_DAY(result) ELSE result END
4391        preserve_eom = expression.args.get("preserve_end_of_month")
4392        result_expr = (
4393            exp.case()
4394            .when(
4395                exp.EQ(this=exp.func("LAST_DAY", this), expression=this),
4396                exp.func("LAST_DAY", date_add_expr),
4397            )
4398            .else_(date_add_expr)
4399            if preserve_eom
4400            else date_add_expr
4401        )
4402
4403        # DuckDB's DATE_ADD function returns TIMESTAMP/DATETIME by default, even when the input is DATE
4404        # To match for example Snowflake's ADD_MONTHS behavior (which preserves the input type)
4405        # We need to cast the result back to the original type when the input is DATE or TIMESTAMPTZ
4406        # Example: ADD_MONTHS('2023-01-31'::date, 1) should return DATE, not TIMESTAMP
4407        if this.is_type(exp.DType.DATE, exp.DType.TIMESTAMPTZ):
4408            return self.sql(exp.Cast(this=result_expr, to=this.type))
4409        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:
4411    def format_sql(self, expression: exp.Format) -> str:
4412        if expression.name.lower() == "%s" and len(expression.expressions) == 1:
4413            return self.func("FORMAT", "'{}'", expression.expressions[0])
4414
4415        return self.function_fallback_sql(expression)
def hexstring_sql( self, expression: sqlglot.expressions.query.HexString, binary_function_repr: str | None = None) -> str:
4417    def hexstring_sql(
4418        self, expression: exp.HexString, binary_function_repr: str | None = None
4419    ) -> str:
4420        # UNHEX('FF') correctly produces blob \xFF in DuckDB
4421        return super().hexstring_sql(expression, binary_function_repr="UNHEX")
def datetrunc_sql(self, expression: sqlglot.expressions.temporal.DateTrunc) -> str:
4423    def datetrunc_sql(self, expression: exp.DateTrunc) -> str:
4424        unit = expression.args.get("unit")
4425        date = expression.this
4426
4427        week_start = _week_trunc_start_dow(unit)
4428        unit = unit_to_str(expression)
4429
4430        if week_start:
4431            result = self.sql(
4432                _build_week_trunc_expression(date, week_start, preserve_start_day=True)
4433            )
4434        else:
4435            result = self.func("DATE_TRUNC", unit, date)
4436
4437        if (
4438            expression.args.get("input_type_preserved")
4439            and date.is_type(*exp.DataType.TEMPORAL_TYPES)
4440            and not (is_date_unit(unit) and date.is_type(exp.DType.DATE))
4441        ):
4442            return self.sql(exp.Cast(this=result, to=date.type))
4443
4444        return result
def datetimetrunc_sql(self, expression: sqlglot.expressions.temporal.DatetimeTrunc) -> str:
4446    def datetimetrunc_sql(self, expression: exp.DatetimeTrunc) -> str:
4447        this = exp.cast(expression.this, exp.DType.DATETIME)
4448        week_start = _week_trunc_start_dow(expression.args.get("unit"))
4449        if week_start:
4450            return self.sql(
4451                _build_week_trunc_expression(
4452                    this, week_start, preserve_start_day=True, cast_to_date=False
4453                )
4454            )
4455
4456        return self.func("DATE_TRUNC", unit_to_str(expression), this)
def timestamptrunc_sql(self, expression: sqlglot.expressions.temporal.TimestampTrunc) -> str:
4458    def timestamptrunc_sql(self, expression: exp.TimestampTrunc) -> str:
4459        zone = expression.args.get("zone")
4460        timestamp = expression.this
4461        week_start = _week_trunc_start_dow(expression.args.get("unit"))
4462
4463        # The week start emulation below is exact, so avoid weekstart_unit_to_str's degrade warning
4464        unit = unit_to_str(expression) if week_start else weekstart_unit_to_str(self, expression)
4465        date_unit = is_date_unit(unit) or bool(week_start)
4466
4467        def _trunc_expr(this: exp.Expr) -> exp.Expr:
4468            if week_start:
4469                return _build_week_trunc_expression(
4470                    this, week_start, preserve_start_day=True, cast_to_date=False
4471                )
4472            return exp.func("DATE_TRUNC", unit, this)
4473
4474        if date_unit and zone:
4475            # BigQuery's TIMESTAMP_TRUNC with timezone truncates in the target timezone and returns as UTC.
4476            # Double AT TIME ZONE needed for BigQuery compatibility:
4477            # 1. First AT TIME ZONE: ensures truncation happens in the target timezone
4478            # 2. Second AT TIME ZONE: converts the DATE result back to TIMESTAMPTZ (preserving time component)
4479            timestamp = exp.AtTimeZone(this=timestamp, zone=zone)
4480            trunced = _trunc_expr(timestamp)
4481            if isinstance(trunced, exp.DateAdd):
4482                # Parenthesize so the trailing AT TIME ZONE binds to the whole shifted expression
4483                trunced = exp.Paren(this=trunced)
4484            return self.sql(exp.AtTimeZone(this=trunced, zone=zone))
4485
4486        result = self.sql(_trunc_expr(timestamp))
4487        if expression.args.get("input_type_preserved"):
4488            if timestamp.type and timestamp.is_type(exp.DType.TIME, exp.DType.TIMETZ):
4489                dummy_date = exp.Cast(
4490                    this=exp.Literal.string("1970-01-01"),
4491                    to=exp.DataType(this=exp.DType.DATE),
4492                )
4493                date_time = exp.Add(this=dummy_date, expression=timestamp)
4494                result = self.func("DATE_TRUNC", unit, date_time)
4495                return self.sql(exp.Cast(this=result, to=timestamp.type))
4496
4497            if timestamp.is_type(*exp.DataType.TEMPORAL_TYPES) and not (
4498                date_unit and timestamp.is_type(exp.DType.DATE)
4499            ):
4500                return self.sql(exp.Cast(this=result, to=timestamp.type))
4501
4502        return result
def trim_sql(self, expression: sqlglot.expressions.string.Trim) -> str:
4504    def trim_sql(self, expression: exp.Trim) -> str:
4505        expression.this.replace(_cast_to_varchar(expression.this))
4506        if expression.expression:
4507            expression.expression.replace(_cast_to_varchar(expression.expression))
4508
4509        result_sql = super().trim_sql(expression)
4510        return _gen_with_cast_to_blob(self, expression, result_sql)
def round_sql(self, expression: sqlglot.expressions.math.Round) -> str:
4512    def round_sql(self, expression: exp.Round) -> str:
4513        this = expression.this
4514        decimals = expression.args.get("decimals")
4515        truncate = expression.args.get("truncate")
4516
4517        # DuckDB requires the scale (decimals) argument to be an INT
4518        # Some dialects (e.g., Snowflake) allow non-integer scales and cast to an integer internally
4519        if decimals is not None and expression.args.get("casts_non_integer_decimals"):
4520            if not (decimals.is_int or decimals.is_type(*exp.DataType.INTEGER_TYPES)):
4521                decimals = exp.cast(decimals, exp.DType.INT)
4522
4523        func = "ROUND"
4524        if truncate:
4525            # BigQuery uses ROUND_HALF_EVEN; Snowflake uses HALF_TO_EVEN
4526            if truncate.this in ("ROUND_HALF_EVEN", "HALF_TO_EVEN"):
4527                func = "ROUND_EVEN"
4528                truncate = None
4529            # BigQuery uses ROUND_HALF_AWAY_FROM_ZERO; Snowflake uses HALF_AWAY_FROM_ZERO
4530            elif truncate.this in ("ROUND_HALF_AWAY_FROM_ZERO", "HALF_AWAY_FROM_ZERO"):
4531                truncate = None
4532
4533        return self.func(func, this, decimals, truncate)
def trycast_sql(self, expression: sqlglot.expressions.functions.TryCast) -> str:
4535    def trycast_sql(self, expression: exp.TryCast) -> str:
4536        to = expression.to
4537        to_type = to.this
4538        src = expression.this
4539
4540        if (
4541            expression.args.get("null_on_text_overflow")
4542            and to_type in exp.DataType.TEXT_TYPES
4543            and to.expressions
4544        ):
4545            return self.sql(
4546                exp.case()
4547                .when(
4548                    exp.LTE(this=exp.func("LENGTH", src), expression=to.expressions[0].this),
4549                    exp.cast(src, "TEXT"),
4550                )
4551                .else_(exp.Null())
4552            )
4553        elif to_type == exp.DType.DATE and expression.args.get("probe_date_format"):
4554            slash_strptime = exp.cast(
4555                exp.func("TRY_STRPTIME", src, exp.Literal.string(self._TRYCAST_DATE_SLASH_FMT)),
4556                "DATE",
4557            )
4558            mon_strptime = exp.cast(
4559                exp.func("TRY_STRPTIME", src, exp.Literal.string(self._TRYCAST_DATE_MON_FMT)),
4560                "DATE",
4561            )
4562            return self.sql(
4563                exp.case()
4564                .when(exp.func("CONTAINS", src, exp.Literal.string("/")), slash_strptime)
4565                .when(
4566                    exp.RegexpLike(this=src, expression=exp.Literal.string("[A-Za-z]")),
4567                    mon_strptime,
4568                )
4569                .else_(exp.TryCast(this=src, to=to))
4570            )
4571        elif (
4572            isinstance(to_type, exp.Interval)
4573            and (unit := to_type.unit)
4574            and expression.args.get("requires_string")
4575        ):
4576            interval_type = exp.DataType.build("INTERVAL")
4577            if isinstance(unit, exp.IntervalSpan):
4578                self.unsupported(
4579                    "TRY_CAST to INTERVAL with span (e.g. HOUR TO MINUTE) is not supported in DuckDB"
4580                )
4581                return self.sql(exp.TryCast(this=src, to=interval_type))
4582            return self.sql(
4583                exp.TryCast(
4584                    this=exp.DPipe(this=src, expression=exp.Literal.string(f" {unit.name}")),
4585                    to=interval_type,
4586                )
4587            )
4588
4589        return super().trycast_sql(expression)
def strtok_sql(self, expression: sqlglot.expressions.string.Strtok) -> str:
4591    def strtok_sql(self, expression: exp.Strtok) -> str:
4592        string_arg = expression.this
4593        delimiter_arg = expression.args.get("delimiter")
4594        part_index_arg = expression.args.get("part_index")
4595
4596        if delimiter_arg and part_index_arg:
4597            # Escape regex chars and build character class at runtime using REGEXP_REPLACE
4598            escaped_delimiter = exp.Anonymous(
4599                this="REGEXP_REPLACE",
4600                expressions=[
4601                    delimiter_arg,
4602                    exp.Literal.string(
4603                        r"([\[\]^.\-*+?(){}|$\\])"
4604                    ),  # Escape problematic regex chars
4605                    exp.Literal.string(
4606                        r"\\\1"
4607                    ),  # Replace with escaped version using $1 backreference
4608                    exp.Literal.string("g"),  # Global flag
4609                ],
4610            )
4611            # CASE WHEN delimiter = '' THEN '' ELSE CONCAT('[', escaped_delimiter, ']') END
4612            regex_pattern = (
4613                exp.case()
4614                .when(delimiter_arg.eq(exp.Literal.string("")), exp.Literal.string(""))
4615                .else_(
4616                    exp.func(
4617                        "CONCAT",
4618                        exp.Literal.string("["),
4619                        escaped_delimiter,
4620                        exp.Literal.string("]"),
4621                    )
4622                )
4623            )
4624
4625            # STRTOK skips empty strings, so we need to filter them out
4626            # LIST_FILTER(REGEXP_SPLIT_TO_ARRAY(string, pattern), x -> x != '')[index]
4627            split_array = exp.func("REGEXP_SPLIT_TO_ARRAY", string_arg, regex_pattern)
4628            x = exp.to_identifier("x")
4629            is_empty = x.eq(exp.Literal.string(""))
4630            filtered_array = exp.func(
4631                "LIST_FILTER",
4632                split_array,
4633                exp.Lambda(this=exp.not_(is_empty.copy()), expressions=[x.copy()]),
4634            )
4635            base_func = exp.Bracket(
4636                this=filtered_array,
4637                expressions=[part_index_arg],
4638                offset=1,
4639            )
4640
4641            # Use template with the built regex pattern
4642            result = exp.replace_placeholders(
4643                self.STRTOK_TEMPLATE.copy(),
4644                string=string_arg,
4645                delimiter=delimiter_arg,
4646                part_index=part_index_arg,
4647                base_func=base_func,
4648            )
4649
4650            return self.sql(result)
4651
4652        return self.function_fallback_sql(expression)
def strtoktoarray_sql(self, expression: sqlglot.expressions.array.StrtokToArray) -> str:
4654    def strtoktoarray_sql(self, expression: exp.StrtokToArray) -> str:
4655        string_arg = expression.this
4656        delimiter_arg = expression.args.get("expression") or exp.Literal.string(" ")
4657
4658        escaped = exp.RegexpReplace(
4659            this=delimiter_arg.copy(),
4660            expression=exp.Literal.string(r"([\[\]^.\-*+?(){}|$\\])"),
4661            replacement=exp.Literal.string(r"\\\1"),
4662            modifiers=exp.Literal.string("g"),
4663        )
4664        return self.sql(
4665            exp.replace_placeholders(
4666                self.STRTOK_TO_ARRAY_TEMPLATE.copy(),
4667                string=string_arg,
4668                delimiter=delimiter_arg,
4669                escaped=escaped,
4670            )
4671        )
def approxquantile_sql(self, expression: sqlglot.expressions.aggregate.ApproxQuantile) -> str:
4673    def approxquantile_sql(self, expression: exp.ApproxQuantile) -> str:
4674        result = self.func("APPROX_QUANTILE", expression.this, expression.args.get("quantile"))
4675
4676        # DuckDB returns integers for APPROX_QUANTILE, cast to DOUBLE if the expected type is a real type
4677        if expression.is_type(*exp.DataType.REAL_TYPES):
4678            result = f"CAST({result} AS DOUBLE)"
4679
4680        return result
def approxquantiles_sql(self, expression: sqlglot.expressions.aggregate.ApproxQuantiles) -> str:
4682    def approxquantiles_sql(self, expression: exp.ApproxQuantiles) -> str:
4683        """
4684        BigQuery's APPROX_QUANTILES(expr, n) returns an array of n+1 approximate quantile values
4685        dividing the input distribution into n equal-sized buckets.
4686
4687        Both BigQuery and DuckDB use approximate algorithms for quantile estimation, but BigQuery
4688        does not document the specific algorithm used so results may differ. DuckDB does not
4689        support RESPECT NULLS.
4690        """
4691        this = expression.this
4692        if isinstance(this, exp.Distinct):
4693            # APPROX_QUANTILES requires 2 args and DISTINCT node grabs both
4694            if len(this.expressions) < 2:
4695                self.unsupported("APPROX_QUANTILES requires a bucket count argument")
4696                return self.function_fallback_sql(expression)
4697            num_quantiles_expr = this.expressions[1].pop()
4698        else:
4699            num_quantiles_expr = expression.expression
4700
4701        if not isinstance(num_quantiles_expr, exp.Literal) or not num_quantiles_expr.is_int:
4702            self.unsupported("APPROX_QUANTILES bucket count must be a positive integer")
4703            return self.function_fallback_sql(expression)
4704
4705        num_quantiles = t.cast(int, num_quantiles_expr.to_py())
4706        if num_quantiles <= 0:
4707            self.unsupported("APPROX_QUANTILES bucket count must be a positive integer")
4708            return self.function_fallback_sql(expression)
4709
4710        quantiles = [
4711            exp.Literal.number(Decimal(i) / Decimal(num_quantiles))
4712            for i in range(num_quantiles + 1)
4713        ]
4714
4715        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:
4717    def jsonextractscalar_sql(self, expression: exp.JSONExtractScalar) -> str:
4718        if expression.args.get("scalar_only"):
4719            json_value = exp.JSONExtractScalar(
4720                this=rename_func("JSON_VALUE")(self, expression), expression="'$'"
4721            )
4722
4723            # `->>` binds looser than most operators, so the wrap logic needs the parent
4724            json_value.parent = expression.parent
4725            expression = json_value
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
SUPPORTS_GROUPING_SETS_AS_SUFFIX
INDEX_ON
INOUT_SEPARATOR
DIRECTED_JOINS
QUERY_HINT_SEP
IS_BOOL_ALLOWED
DUPLICATE_KEY_UPDATE_WITH_SET
LIMIT_IS_TOP
RETURNING_END
EXTRACT_ALLOWS_QUOTES
TZ_TO_WITH_TIME_ZONE
VALUES_AS_TABLE
ALTER_TABLE_INCLUDE_COLUMN_KEYWORD
UNNEST_WITH_ORDINALITY
COMPUTED_COLUMN_WITH_TYPE
SUPPORTS_TABLE_COPY
TABLESAMPLE_REQUIRES_PARENS
TABLESAMPLE_SIZE_IS_ROWS
TABLESAMPLE_WITH_METHOD
COLLATE_IS_FUNC
DATA_TYPE_SPECIFIERS_ALLOWED
ENSURE_BOOLS
CTE_RECURSIVE_KEYWORD_REQUIRED
SUPPORTS_SINGLE_ARG_CONCAT
SUPPORTS_TABLE_ALIAS_COLUMNS
SUPPORTS_NAMED_CTE_COLUMNS
UNPIVOT_ALIASES_ARE_IDENTIFIERS
PIVOT_ALIAS_WITH_AS
INSERT_OVERWRITE
SUPPORTS_SELECT_INTO
SUPPORTS_UNLOGGED_TABLES
SUPPORTS_MODIFY_COLUMN
SUPPORTS_CHANGE_COLUMN
SUPPORTS_ALTER_COLUMN_NULLABILITY
SUPPORTS_ALTER_COLUMN_IF_EXISTS
LIKE_PROPERTY_INSIDE_SCHEMA
JSON_TYPE_REQUIRED_FOR_EXTRACTION
JSON_PATH_SINGLE_QUOTE_ESCAPE
JSON_PATH_KEY_QUOTED_FORCES_BRACKETS
SET_OP_MODIFIERS
SET_OP_LIMITS
SET_OP_PARENTHESIZED_OPERANDS
COPY_PARAMS_ARE_WRAPPED
COPY_PARAMS_EQ_REQUIRED
TRY_SUPPORTED
SUPPORTS_UESCAPE
UNICODE_SUBSTITUTE
HEX_FUNC
WITH_PROPERTIES_PREFIX
QUOTE_JSON_PATH
SUPPORTS_EXPLODING_PROJECTIONS
ARRAY_CONCAT_IS_VAR_LEN
SUPPORTS_CONVERT_TIMEZONE
SUPPORTS_MEDIAN
SUPPORTS_UNIX_SECONDS
ALTER_SET_WRAPPED
PARSE_JSON_NAME
ARRAY_SIZE_NAME
ALTER_SET_TYPE
SUPPORTS_BETWEEN_FLAGS
MATCH_AGAINST_TABLE_PREFIX
DECLARE_DEFAULT_ASSIGNMENT
UPDATE_STATEMENT_SUPPORTS_FROM
STAR_EXCLUDE_REQUIRES_DERIVED_TABLE
UNSUPPORTED_TYPES
TIME_PART_SINGULARS
TOKEN_MAPPING
EXPRESSION_PRECEDES_PROPERTIES_CREATABLES
WITH_SEPARATED_COMMENTS
EXCLUDE_COMMENTS
PARAMETERIZABLE_TEXT_TYPES
EXPRESSIONS_WITHOUT_NESTED_CTES
RESPECT_IGNORE_NULLS_UNSUPPORTED_EXPRESSIONS
MOD_OPERATOR
MOD_PAREN_PARENT_TYPES
SAFE_JSON_PATH_KEY_RE
SENTINEL_LINE_BREAK
pretty
identify
normalize
pad
unsupported_level
max_unsupported
leading_comma
max_text_width
comments
dialect
normalize_functions
unsupported_messages
generate
preprocess
unsupported
sep
seg
sanitize_comment
maybe_comment
wrap
no_identify
normalize_func
indent
sql
uncache_sql
cache_sql
characterset_sql
column_parts
column_sql
pseudocolumn_sql
columnposition_sql
columndef_sql
columnconstraint_sql
computedcolumnconstraint_sql
compresscolumnconstraint_sql
generatedasidentitycolumnconstraint_sql
generatedasrowcolumnconstraint_sql
periodforsystemtimeconstraint_sql
notnullcolumnconstraint_sql
primarykeycolumnconstraint_sql
uniquecolumnconstraint_sql
inoutcolumnconstraint_sql
createable_sql
create_sql
sequenceproperties_sql
triggerproperties_sql
triggerreferencing_sql
triggerevent_sql
clone_sql
describe_sql
heredoc_sql
prepend_ctes
with_sql
cte_sql
tablealias_sql
bitstring_sql
bytestring_sql
unicodestring_sql
rawstring_sql
datatypeparam_sql
datatype_param_bound_limiter
directory_sql
delete_sql
drop_sql
set_operation
set_operations
fetch_sql
limitoptions_sql
hint_sql
indexparameters_sql
index_sql
dynamicidentifier_sql
identifier_sql
lowerhex_sql
inputoutputformat_sql
national_sql
partition_sql
properties_sql
root_properties
properties
with_properties
locate_properties
property_name
property_sql
uuidproperty_sql
likeproperty_sql
fallbackproperty_sql
journalproperty_sql
freespaceproperty_sql
checksumproperty_sql
mergeblockratioproperty_sql
moduleproperty_sql
datablocksizeproperty_sql
blockcompressionproperty_sql
isolatedloadingproperty_sql
partitionboundspec_sql
partitionedofproperty_sql
lockingproperty_sql
withdataproperty_sql
withsystemversioningproperty_sql
insert_sql
introducer_sql
kill_sql
pseudotype_sql
objectidentifier_sql
onconflict_sql
returning_sql
rowformatdelimitedproperty_sql
withtablehint_sql
indextablehint_sql
historicaldata_sql
table_parts
table_sql
pivot_sql
version_sql
tuple_sql
update_sql
values_sql
var_sql
into_sql
from_sql
groupingsets_sql
rollup_sql
rollupindex_sql
rollupproperty_sql
cube_sql
group_sql
having_sql
connect_sql
prior_sql
lateral_op
lateral_sql
limit_sql
offset_sql
setitem_sql
set_sql
queryband_sql
pragma_sql
lock_sql
literal_sql
escape_str
loaddata_sql
null_sql
boolean_sql
booland_sql
boolor_sql
order_sql
withfill_sql
cluster_sql
clusterproperty_sql
distribute_sql
sort_sql
ordered_sql
matchrecognizemeasure_sql
matchrecognize_sql
query_modifiers
options_modifier
forclause_sql
queryoption_sql
offset_limit_modifiers
after_limit_modifiers
select_sql
schema_sql
schema_columns_sql
star_sql
parameter_sql
sessionparameter_sql
placeholder_sql
subquery_sql
qualify_sql
prewhere_sql
where_sql
partition_by_sql
windowspec_sql
between_sql
bracket_offset_expressions
all_sql
any_sql
exists_sql
case_sql
constraint_sql
nextvaluefor_sql
convert_concat_args
concat_sql
check_sql
foreignkey_sql
primarykey_sql
timeserieskey_sql
if_sql
matchagainst_sql
jsonkeyvalue_sql
jsonpath_sql
json_path_part
formatjson_sql
formatphrase_sql
jsonarray_sql
jsonarrayagg_sql
jsoncolumndef_sql
jsonschema_sql
jsontable_sql
openjsoncolumndef_sql
openjson_sql
in_unnest_op
interval_sql
return_sql
reference_sql
anonymous_sql
paren_sql
neg_sql
not_sql
alias_sql
pivotalias_sql
atindex_sql
attimezone_sql
fromtimezone_sql
fromiso8601date_sql
fromiso8601timestamp_sql
fromiso8601timestampnanos_sql
add_sql
and_sql
or_sql
xor_sql
connector_sql
bitwiseand_sql
bitwiseleftshift_sql
bitwiseor_sql
bitwiserightshift_sql
cast_sql
command_sql
comment_sql
mergetreettlaction_sql
mergetreettl_sql
transaction_sql
commit_sql
rollback_sql
altercolumn_sql
modifycolumn_sql
alterindex_sql
alterdiststyle_sql
altersortkey_sql
alterrename_sql
renamecolumn_sql
alterset_sql
alter_sql
altersession_sql
add_column_sql
droppartition_sql
dropprimarykey_sql
addconstraint_sql
addpartition_sql
distinct_sql
havingmax_sql
intdiv_sql
dpipe_sql
div_sql
safedivide_sql
overlaps_sql
distance_sql
distancend_sql
dot_sql
eq_sql
propertyeq_sql
escape_sql
glob_sql
gt_sql
gte_sql
is_sql
like_sql
ilike_sql
match_sql
similarto_sql
lt_sql
lte_sql
mod_sql
mul_sql
neq_sql
nullsafeeq_sql
nullsafeneq_sql
sub_sql
jsoncast_sql
try_sql
log_sql
use_sql
binary
ceil_floor
function_fallback_sql
func
format_args
too_wide
format_time
expressions
op_expressions
naked_property
tag_sql
token_sql
userdefinedfunction_sql
macrooverloads_sql
macrooverload_sql
joinhint_sql
kwarg_sql
when_sql
whens_sql
merge_sql
tochar_sql
dictproperty_sql
dictrange_sql
dictsubproperty_sql
duplicatekeyproperty_sql
uniquekeyproperty_sql
distributedbyproperty_sql
oncluster_sql
clusteredbyproperty_sql
anyvalue_sql
querytransform_sql
indexconstraintoption_sql
checkcolumnconstraint_sql
indexcolumnconstraint_sql
nvl2_sql
nthvalue_sql
comprehension_sql
columnprefix_sql
opclass_sql
predict_sql
generateembedding_sql
generatetext_sql
generatetable_sql
generatebool_sql
generateint_sql
generatedouble_sql
mltranslate_sql
mlforecast_sql
aiforecast_sql
featuresattime_sql
vectorsearch_sql
forin_sql
refresh_sql
toarray_sql
tsordstotimestamp_sql
tsordstodatetime_sql
tsordstodate_sql
unixdate_sql
lastday_sql
dateadd_sql
arrayinsert_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