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

Snowflake aligns slices to the 1970-01-01 epoch, so an explicit origin is required because DuckDB's default origin differs. For 'END' kind, add the interval to get the next slice's start; for DATE input, cast back to DATE.

def unicodestring_sql(self, expression: sqlglot.expressions.query.UnicodeString) -> str:
2468    def unicodestring_sql(self, expression: exp.UnicodeString) -> str:
2469        # DuckDB has no Unicode escape strings and doesn't read \u in a string, so the escapes
2470        # are decoded into the characters, e.g. U&'d\0061t\0061' and U&'d!+000061ta' UESCAPE '!'
2471        # are both 'data'
2472        escape = expression.args.get("escape")
2473        escape_char = re.escape(escape.name if isinstance(escape, exp.Expr) else "\\")
2474        pattern = re.compile(
2475            rf"{escape_char}(?:\+([0-9a-fA-F]{{6}})|([0-9a-fA-F]{{4}})|({escape_char}))"
2476        )
2477
2478        def decode(match: re.Match[str]) -> str:
2479            if match.group(3):
2480                return match.group(3)
2481            return chr(int(match.group(1) or match.group(2), 16))
2482
2483        # A character outside the BMP can be written as a UTF-16 surrogate pair
2484        text = (
2485            pattern.sub(decode, expression.name).encode("utf-16", "surrogatepass").decode("utf-16")
2486        )
2487        return self.sql(exp.Literal.string(text))
def bytestring_sql(self, expression: sqlglot.expressions.query.ByteString) -> str:
2489    def bytestring_sql(self, expression: exp.ByteString) -> str:
2490        if not expression.args.get("is_bytes"):
2491            return super().bytestring_sql(expression)
2492
2493        # Each char is a byte. Casting to BLOB decodes \xHH, so everything but printable ASCII
2494        # (including the backslash itself) is written as such
2495        text = "".join(
2496            ch if ch.isascii() and ch.isprintable() and ch != "\\" else f"\\x{ord(ch):02x}"
2497            for ch in expression.this
2498        )
2499        return self.sql(exp.cast(exp.Literal.string(text), exp.DType.BINARY))
def bitmapbucketnumber_sql(self, expression: sqlglot.expressions.math.BitmapBucketNumber) -> str:
2501    def bitmapbucketnumber_sql(self, expression: exp.BitmapBucketNumber) -> str:
2502        """
2503        Transpile BITMAP_BUCKET_NUMBER function from Snowflake to DuckDB equivalent.
2504
2505        Snowflake's BITMAP_BUCKET_NUMBER returns a 1-based bucket identifier where:
2506        - Each bucket covers 32,768 values
2507        - Bucket numbering starts at 1
2508        - Formula: ((value - 1) // 32768) + 1 for positive values
2509
2510        For non-positive values (0 and negative), we use value // 32768 to avoid
2511        producing bucket 0 or positive bucket IDs for negative inputs.
2512        """
2513        value = expression.this
2514
2515        positive_formula = ((value - 1) // 32768) + 1
2516        non_positive_formula = value // 32768
2517
2518        # CASE WHEN value > 0 THEN ((value - 1) // 32768) + 1 ELSE value // 32768 END
2519        case_expr = (
2520            exp.case()
2521            .when(exp.GT(this=value, expression=exp.Literal.number(0)), positive_formula)
2522            .else_(non_positive_formula)
2523        )
2524        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:
2526    def bitmapbitposition_sql(self, expression: exp.BitmapBitPosition) -> str:
2527        """
2528        Transpile Snowflake's BITMAP_BIT_POSITION to DuckDB CASE expression.
2529
2530        Snowflake's BITMAP_BIT_POSITION behavior:
2531        - For n <= 0: returns ABS(n) % 32768
2532        - For n > 0: returns (n - 1) % 32768 (maximum return value is 32767)
2533        """
2534        this = expression.this
2535
2536        return self.sql(
2537            exp.Mod(
2538                this=exp.Paren(
2539                    this=exp.If(
2540                        this=exp.GT(this=this, expression=exp.Literal.number(0)),
2541                        true=this - exp.Literal.number(1),
2542                        false=exp.Abs(this=this),
2543                    )
2544                ),
2545                expression=MAX_BIT_POSITION,
2546            )
2547        )

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:
2549    def bitmapconstructagg_sql(self, expression: exp.BitmapConstructAgg) -> str:
2550        """
2551        Transpile Snowflake's BITMAP_CONSTRUCT_AGG to DuckDB equivalent.
2552        Uses a pre-parsed template with placeholders replaced by expression nodes.
2553
2554        Snowflake bitmap format:
2555        - Small (< 5 unique values): 2-byte count (big-endian) + values (little-endian) + padding to 10 bytes
2556        - Large (>= 5 unique values): 10-byte header (0x08 + 9 zeros) + values (little-endian)
2557        """
2558        arg = expression.this
2559        return (
2560            f"({self.sql(exp.replace_placeholders(self.BITMAP_CONSTRUCT_AGG_TEMPLATE, arg=arg))})"
2561        )

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:
2563    def getignorecase_sql(self, expression: exp.GetIgnoreCase) -> str:
2564        self.unsupported("DuckDB does not support the GET_IGNORE_CASE() function")
2565        return self.function_fallback_sql(expression)
def compress_sql(self, expression: sqlglot.expressions.string.Compress) -> str:
2567    def compress_sql(self, expression: exp.Compress) -> str:
2568        self.unsupported("DuckDB does not support the COMPRESS() function")
2569        return self.function_fallback_sql(expression)
def encrypt_sql(self, expression: sqlglot.expressions.string.Encrypt) -> str:
2571    def encrypt_sql(self, expression: exp.Encrypt) -> str:
2572        self.unsupported("ENCRYPT is not supported in DuckDB")
2573        return self.function_fallback_sql(expression)
def decrypt_sql(self, expression: sqlglot.expressions.string.Decrypt) -> str:
2575    def decrypt_sql(self, expression: exp.Decrypt) -> str:
2576        func_name = "TRY_DECRYPT" if expression.args.get("safe") else "DECRYPT"
2577        self.unsupported(f"{func_name} is not supported in DuckDB")
2578        return self.function_fallback_sql(expression)
def decryptraw_sql(self, expression: sqlglot.expressions.string.DecryptRaw) -> str:
2580    def decryptraw_sql(self, expression: exp.DecryptRaw) -> str:
2581        func_name = "TRY_DECRYPT_RAW" if expression.args.get("safe") else "DECRYPT_RAW"
2582        self.unsupported(f"{func_name} is not supported in DuckDB")
2583        return self.function_fallback_sql(expression)
def encryptraw_sql(self, expression: sqlglot.expressions.string.EncryptRaw) -> str:
2585    def encryptraw_sql(self, expression: exp.EncryptRaw) -> str:
2586        self.unsupported("ENCRYPT_RAW is not supported in DuckDB")
2587        return self.function_fallback_sql(expression)
def parseurl_sql(self, expression: sqlglot.expressions.string.ParseUrl) -> str:
2589    def parseurl_sql(self, expression: exp.ParseUrl) -> str:
2590        self.unsupported("PARSE_URL is not supported in DuckDB")
2591        return self.function_fallback_sql(expression)
def parseip_sql(self, expression: sqlglot.expressions.functions.ParseIp) -> str:
2593    def parseip_sql(self, expression: exp.ParseIp) -> str:
2594        self.unsupported("PARSE_IP is not supported in DuckDB")
2595        return self.function_fallback_sql(expression)
def decompressstring_sql(self, expression: sqlglot.expressions.string.DecompressString) -> str:
2597    def decompressstring_sql(self, expression: exp.DecompressString) -> str:
2598        self.unsupported("DECOMPRESS_STRING is not supported in DuckDB")
2599        return self.function_fallback_sql(expression)
def decompressbinary_sql(self, expression: sqlglot.expressions.string.DecompressBinary) -> str:
2601    def decompressbinary_sql(self, expression: exp.DecompressBinary) -> str:
2602        self.unsupported("DECOMPRESS_BINARY is not supported in DuckDB")
2603        return self.function_fallback_sql(expression)
def jarowinklersimilarity_sql(self, expression: sqlglot.expressions.math.JarowinklerSimilarity) -> str:
2605    def jarowinklersimilarity_sql(self, expression: exp.JarowinklerSimilarity) -> str:
2606        this = expression.this
2607        expr = expression.expression
2608
2609        if expression.args.get("case_insensitive"):
2610            this = exp.Upper(this=this)
2611            expr = exp.Upper(this=expr)
2612
2613        result = exp.func("JARO_WINKLER_SIMILARITY", this, expr)
2614
2615        if expression.args.get("integer_scale"):
2616            result = exp.cast(result * 100, "INTEGER")
2617
2618        return self.sql(result)
def randstr_sql(self, expression: sqlglot.expressions.functions.Randstr) -> str:
2620    def randstr_sql(self, expression: exp.Randstr) -> str:
2621        """
2622        Transpile Snowflake's RANDSTR to DuckDB equivalent using deterministic hash-based random.
2623        Uses a pre-parsed template with placeholders replaced by expression nodes.
2624
2625        RANDSTR(length, generator) generates a random string of specified length.
2626        - With numeric seed: Use HASH(i + seed) for deterministic output (same seed = same result)
2627        - With RANDOM(): Use RANDOM() in the hash for non-deterministic output
2628        - No generator: Use default seed value
2629        """
2630        length = expression.this
2631        generator = expression.args.get("generator")
2632
2633        if generator:
2634            if isinstance(generator, exp.Rand):
2635                # If it's RANDOM(), use its seed if available, otherwise use RANDOM() itself
2636                seed_value = generator.this or generator
2637            else:
2638                # Const/int or other expression - use as seed directly
2639                seed_value = generator
2640        else:
2641            # No generator specified, use default seed (arbitrary but deterministic)
2642            seed_value = exp.Literal.number(RANDSTR_SEED)
2643
2644        replacements = {"seed": seed_value, "length": length}
2645        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:
2647    @unsupported_args("finish")
2648    def reduce_sql(self, expression: exp.Reduce) -> str:
2649        array_arg = expression.this
2650        initial_value = expression.args.get("initial")
2651        merge_lambda = expression.args.get("merge")
2652
2653        return self.func("list_reduce", array_arg, merge_lambda, initial_value)
def zipf_sql(self, expression: sqlglot.expressions.functions.Zipf) -> str:
2655    def zipf_sql(self, expression: exp.Zipf) -> str:
2656        """
2657        Transpile Snowflake's ZIPF to DuckDB using CDF-based inverse sampling.
2658        Uses a pre-parsed template with placeholders replaced by expression nodes.
2659        """
2660        s = expression.this
2661        n = expression.args["elementcount"]
2662        gen = expression.args["gen"]
2663
2664        if not isinstance(gen, exp.Rand):
2665            # (ABS(HASH(seed)) % 1000000) / 1000000.0
2666            random_expr: exp.Expr = exp.Div(
2667                this=exp.Paren(
2668                    this=exp.Mod(
2669                        this=exp.Abs(this=exp.Anonymous(this="HASH", expressions=[gen.copy()])),
2670                        expression=exp.Literal.number(1000000),
2671                    )
2672                ),
2673                expression=exp.Literal.number(1000000.0),
2674            )
2675        else:
2676            # Use RANDOM() for non-deterministic output
2677            random_expr = exp.Rand()
2678
2679        replacements = {"s": s, "n": n, "random_expr": random_expr}
2680        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:
2682    def tobinary_sql(self, expression: exp.ToBinary) -> str:
2683        """
2684        TO_BINARY and TRY_TO_BINARY transpilation:
2685        - 'HEX': TO_BINARY('48454C50', 'HEX') -> UNHEX('48454C50')
2686        - 'UTF-8': TO_BINARY('TEST', 'UTF-8') -> ENCODE('TEST')
2687        - 'BASE64': TO_BINARY('SEVMUA==', 'BASE64') -> FROM_BASE64('SEVMUA==')
2688
2689        For TRY_TO_BINARY (safe=True), wrap with TRY():
2690        - 'HEX': TRY_TO_BINARY('invalid', 'HEX') -> TRY(UNHEX('invalid'))
2691        """
2692        value = expression.this
2693        format_arg = expression.args.get("format")
2694        is_safe = expression.args.get("safe")
2695        is_binary = _is_binary(expression)
2696
2697        if not format_arg and not is_binary:
2698            func_name = "TRY_TO_BINARY" if is_safe else "TO_BINARY"
2699            return self.func(func_name, value)
2700
2701        # Snowflake defaults to HEX encoding when no format is specified
2702        fmt = format_arg.name.upper() if format_arg else "HEX"
2703
2704        if fmt in ("UTF-8", "UTF8"):
2705            # DuckDB ENCODE always uses UTF-8, no charset parameter needed
2706            result = self.func("ENCODE", value)
2707        elif fmt == "BASE64":
2708            result = self.func("FROM_BASE64", value)
2709        elif fmt == "HEX":
2710            result = self.func("UNHEX", value)
2711        else:
2712            if is_safe:
2713                return self.sql(exp.null())
2714            else:
2715                self.unsupported(f"format {fmt} is not supported")
2716                result = self.func("TO_BINARY", value)
2717        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:
2719    def tonumber_sql(self, expression: exp.ToNumber) -> str:
2720        fmt = expression.args.get("format")
2721        precision = expression.args.get("precision")
2722        scale = expression.args.get("scale")
2723
2724        if not fmt and precision and scale:
2725            to = exp.DataType.build(f"DECIMAL({precision.name}, {scale.name})", dialect="duckdb")
2726            if expression.args.get("safe"):
2727                return self.sql(exp.TryCast(this=expression.this, to=to))
2728            return self.sql(exp.cast(expression.this, to, dialect="duckdb"))
2729
2730        return super().tonumber_sql(expression)
def generator_sql(self, expression: sqlglot.expressions.array.Generator) -> str:
2756    def generator_sql(self, expression: exp.Generator) -> str:
2757        # Transpile Snowflake GENERATOR to DuckDB range()
2758        rowcount = expression.args.get("rowcount")
2759        time_limit = expression.args.get("time_limit")
2760
2761        if time_limit:
2762            self.unsupported("GENERATOR TIMELIMIT parameter is not supported in DuckDB")
2763
2764        if not rowcount:
2765            self.unsupported("GENERATOR without ROWCOUNT is not supported in DuckDB")
2766            return self.func("range", exp.Literal.number(0))
2767
2768        return self.func("range", rowcount)
def greatest_sql(self, expression: sqlglot.expressions.functions.Greatest) -> str:
2770    def greatest_sql(self, expression: exp.Greatest) -> str:
2771        return self._greatest_least_sql(expression)
def least_sql(self, expression: sqlglot.expressions.functions.Least) -> str:
2773    def least_sql(self, expression: exp.Least) -> str:
2774        return self._greatest_least_sql(expression)
def lambda_sql( self, expression: sqlglot.expressions.query.Lambda, arrow_sep: str = '->', wrap: bool = True) -> str:
2776    def lambda_sql(self, expression: exp.Lambda, arrow_sep: str = "->", wrap: bool = True) -> str:
2777        if self.dialect.version >= (1, 3):
2778            prefix = "LAMBDA "
2779            arrow_sep = ":"
2780            wrap = False
2781        else:
2782            prefix = ""
2783
2784        lambda_sql = super().lambda_sql(expression, arrow_sep=arrow_sep, wrap=wrap)
2785        return f"{prefix}{lambda_sql}"
def show_sql(self, expression: sqlglot.expressions.ddl.Show) -> str:
2787    def show_sql(self, expression: exp.Show) -> str:
2788        from_ = self.sql(expression, "from_")
2789        from_ = f" FROM {from_}" if from_ else ""
2790        return f"SHOW {expression.name}{from_}"
def soundex_sql(self, expression: sqlglot.expressions.string.Soundex) -> str:
2792    def soundex_sql(self, expression: exp.Soundex) -> str:
2793        self.unsupported("SOUNDEX is not supported in DuckDB")
2794        return self.func("SOUNDEX", expression.this)
def placeholder_sql(self, expression: sqlglot.expressions.core.Placeholder) -> str:
2796    def placeholder_sql(self, expression: exp.Placeholder) -> str:
2797        if isinstance(expression.this, exp.Identifier):
2798            return f"${self.sql(expression, 'this')}"
2799        return super().placeholder_sql(expression)
def install_sql(self, expression: sqlglot.expressions.ddl.Install) -> str:
2801    def install_sql(self, expression: exp.Install) -> str:
2802        force = "FORCE " if expression.args.get("force") else ""
2803        this = self.sql(expression, "this")
2804        from_clause = expression.args.get("from_")
2805        from_clause = f" FROM {from_clause}" if from_clause else ""
2806        return f"{force}INSTALL {this}{from_clause}"
def approxtopk_sql(self, expression: sqlglot.expressions.aggregate.ApproxTopK) -> str:
2808    def approxtopk_sql(self, expression: exp.ApproxTopK) -> str:
2809        self.unsupported(
2810            "APPROX_TOP_K cannot be transpiled to DuckDB due to incompatible return types. "
2811        )
2812        return self.function_fallback_sql(expression)
def strposition_sql(self, expression: sqlglot.expressions.string.StrPosition) -> str:
2814    def strposition_sql(self, expression: exp.StrPosition) -> str:
2815        this = expression.this
2816        substr = expression.args.get("substr")
2817        position = expression.args.get("position")
2818
2819        # For BINARY/BLOB: DuckDB's STRPOS doesn't support BLOB types
2820        # Convert to HEX strings, use STRPOS, then convert hex position to byte position
2821        if _is_binary(this):
2822            # Build expression: STRPOS(HEX(haystack), HEX(needle))
2823            hex_strpos = exp.StrPosition(
2824                this=exp.Hex(this=this),
2825                substr=exp.Hex(this=substr),
2826            )
2827
2828            return self.sql(exp.cast((hex_strpos + 1) / 2, exp.DType.INT))
2829
2830        # For VARCHAR: handle clamp_position
2831        if expression.args.get("clamp_position") and position:
2832            expression = expression.copy()
2833            expression.set(
2834                "position",
2835                exp.If(
2836                    this=exp.LTE(this=position, expression=exp.Literal.number(0)),
2837                    true=exp.Literal.number(1),
2838                    false=position.copy(),
2839                ),
2840            )
2841
2842        return strposition_sql(self, expression)
def substring_sql(self, expression: sqlglot.expressions.string.Substring) -> str:
2844    def substring_sql(self, expression: exp.Substring) -> str:
2845        if expression.args.get("zero_start"):
2846            start = expression.args.get("start")
2847            length = expression.args.get("length")
2848
2849            if start := expression.args.get("start"):
2850                start = exp.If(this=start.eq(0), true=exp.Literal.number(1), false=start)
2851            if length := expression.args.get("length"):
2852                length = exp.If(this=length < 0, true=exp.Literal.number(0), false=length)
2853
2854            return self.func("SUBSTRING", expression.this, start, length)
2855
2856        return self.function_fallback_sql(expression)
def strtotime_sql(self, expression: sqlglot.expressions.temporal.StrToTime) -> str:
2858    def strtotime_sql(self, expression: exp.StrToTime) -> str:
2859        # Check if target_type requires TIMESTAMPTZ (for LTZ/TZ variants)
2860        target_type = expression.args.get("target_type")
2861        needs_tz = target_type and target_type.this in (
2862            exp.DType.TIMESTAMPLTZ,
2863            exp.DType.TIMESTAMPTZ,
2864        )
2865
2866        value, formatted_time = self._strptime_default_year(expression)
2867
2868        if expression.args.get("safe"):
2869            cast_type = exp.DType.TIMESTAMPTZ if needs_tz else exp.DType.TIMESTAMP
2870            return self.sql(exp.cast(self.func("TRY_STRPTIME", value, formatted_time), cast_type))
2871
2872        base_sql = self.func("STRPTIME", value, formatted_time)
2873        if needs_tz:
2874            return self.sql(
2875                exp.cast(
2876                    base_sql,
2877                    exp.DataType(this=exp.DType.TIMESTAMPTZ),
2878                )
2879            )
2880        return base_sql
def strtodate_sql(self, expression: sqlglot.expressions.temporal.StrToDate) -> str:
2882    def strtodate_sql(self, expression: exp.StrToDate) -> str:
2883        value, formatted_time = self._strptime_default_year(expression)
2884        function_name = "STRPTIME" if not expression.args.get("safe") else "TRY_STRPTIME"
2885        return self.sql(
2886            exp.cast(
2887                self.func(function_name, value, formatted_time),
2888                exp.DataType(this=exp.DType.DATE),
2889            )
2890        )
def parsedatetime_sql(self, expression: sqlglot.expressions.temporal.ParseDatetime) -> str:
2904    def parsedatetime_sql(self, expression: exp.ParseDatetime) -> str:
2905        value, formatted_time = self._strptime_default_year(expression)
2906        return self.func("STRPTIME", value, formatted_time)
def parsetime_sql(self, expression: sqlglot.expressions.temporal.ParseTime) -> str:
2908    def parsetime_sql(self, expression: exp.ParseTime) -> str:
2909        formatted_time = self.format_time(expression)
2910        return self.sql(
2911            exp.cast(
2912                self.func("STRPTIME", expression.this, formatted_time),
2913                exp.DataType(this=exp.DType.TIME),
2914            )
2915        )
def tsordstotime_sql(self, expression: sqlglot.expressions.temporal.TsOrDsToTime) -> str:
2917    def tsordstotime_sql(self, expression: exp.TsOrDsToTime) -> str:
2918        this = expression.this
2919        time_format = self.format_time(expression)
2920        safe = expression.args.get("safe")
2921        time_type = exp.DataType.from_str("TIME", dialect="duckdb")
2922        cast_expr = exp.TryCast if safe else exp.Cast
2923
2924        if time_format:
2925            func_name = "TRY_STRPTIME" if safe else "STRPTIME"
2926            strptime = exp.Anonymous(this=func_name, expressions=[this, time_format])
2927            return self.sql(cast_expr(this=strptime, to=time_type))
2928
2929        if isinstance(this, exp.TsOrDsToTime) or this.is_type(exp.DType.TIME):
2930            return self.sql(this)
2931
2932        return self.sql(cast_expr(this=this, to=time_type))
def currentdate_sql(self, expression: sqlglot.expressions.temporal.CurrentDate) -> str:
2934    def currentdate_sql(self, expression: exp.CurrentDate) -> str:
2935        if not expression.this:
2936            return "CURRENT_DATE"
2937
2938        expr = exp.Cast(
2939            this=exp.AtTimeZone(this=exp.CurrentTimestamp(), zone=expression.this),
2940            to=exp.DataType(this=exp.DType.DATE),
2941        )
2942        return self.sql(expr)
def checkjson_sql(self, expression: sqlglot.expressions.json.CheckJson) -> str:
2944    def checkjson_sql(self, expression: exp.CheckJson) -> str:
2945        arg = expression.this
2946        return self.sql(
2947            exp.case()
2948            .when(
2949                exp.or_(arg.is_(exp.Null()), arg.eq(""), exp.func("json_valid", arg)),
2950                exp.null(),
2951            )
2952            .else_(exp.Literal.string("Invalid JSON"))
2953        )
def parsejson_sql(self, expression: sqlglot.expressions.json.ParseJSON) -> str:
2955    def parsejson_sql(self, expression: exp.ParseJSON) -> str:
2956        arg = expression.this
2957        if expression.args.get("safe"):
2958            return self.sql(
2959                exp.case()
2960                .when(exp.func("json_valid", arg), exp.cast(arg.copy(), "JSON"))
2961                .else_(exp.null())
2962            )
2963        return self.func("JSON", arg)
def unicode_sql(self, expression: sqlglot.expressions.string.Unicode) -> str:
2965    def unicode_sql(self, expression: exp.Unicode) -> str:
2966        if expression.args.get("empty_is_zero"):
2967            return self.sql(
2968                exp.case()
2969                .when(expression.this.eq(exp.Literal.string("")), exp.Literal.number(0))
2970                .else_(exp.Anonymous(this="UNICODE", expressions=[expression.this]))
2971            )
2972
2973        return self.func("UNICODE", expression.this)
def stripnullvalue_sql(self, expression: sqlglot.expressions.json.StripNullValue) -> str:
2975    def stripnullvalue_sql(self, expression: exp.StripNullValue) -> str:
2976        return self.sql(
2977            exp.case()
2978            .when(exp.func("json_type", expression.this).eq("NULL"), exp.null())
2979            .else_(expression.this)
2980        )
def trunc_sql(self, expression: sqlglot.expressions.math.Trunc) -> str:
2982    def trunc_sql(self, expression: exp.Trunc) -> str:
2983        decimals = expression.args.get("decimals")
2984        if (
2985            expression.args.get("fractions_supported")
2986            and decimals
2987            and not decimals.is_type(exp.DType.INT)
2988        ):
2989            decimals = exp.cast(decimals, exp.DType.INT, dialect="duckdb")
2990
2991        return self.func("TRUNC", expression.this, decimals)
def normal_sql(self, expression: sqlglot.expressions.functions.Normal) -> str:
2993    def normal_sql(self, expression: exp.Normal) -> str:
2994        """
2995        Transpile Snowflake's NORMAL(mean, stddev, gen) to DuckDB.
2996
2997        Uses the Box-Muller transform via NORMAL_TEMPLATE.
2998        """
2999        mean = expression.this
3000        stddev = expression.args["stddev"]
3001        gen: exp.Expr = expression.args["gen"]
3002
3003        # Build two uniform random values [0, 1) for Box-Muller transform
3004        if isinstance(gen, exp.Rand) and gen.this is None:
3005            u1: exp.Expr = exp.Rand()
3006            u2: exp.Expr = exp.Rand()
3007        else:
3008            # Seeded: derive two values using HASH with different inputs
3009            seed = gen.this if isinstance(gen, exp.Rand) else gen
3010            u1 = exp.replace_placeholders(self.SEEDED_RANDOM_TEMPLATE, seed=seed)
3011            u2 = exp.replace_placeholders(
3012                self.SEEDED_RANDOM_TEMPLATE,
3013                seed=exp.Add(this=seed.copy(), expression=exp.Literal.number(1)),
3014            )
3015
3016        replacements = {"mean": mean, "stddev": stddev, "u1": u1, "u2": u2}
3017        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:
3019    def uniform_sql(self, expression: exp.Uniform) -> str:
3020        """
3021        Transpile Snowflake's UNIFORM(min, max, gen) to DuckDB.
3022
3023        UNIFORM returns a random value in [min, max]:
3024        - Integer result if both min and max are integers
3025        - Float result if either min or max is a float
3026        """
3027        min_val = expression.this
3028        max_val = expression.expression
3029        gen = expression.args.get("gen") or expression.args.get("seed") or exp.Rand()
3030
3031        # Determine if result should be integer (both bounds are integers).
3032        # We do this to emulate Snowflake's behavior, INT -> INT, FLOAT -> FLOAT
3033        is_int_result = min_val.is_int and max_val.is_int
3034
3035        # Build the random value expression [0, 1)
3036        if not isinstance(gen, exp.Rand):
3037            # Seed value: (ABS(HASH(seed)) % 1000000) / 1000000.0
3038            random_expr: exp.Expr = exp.Div(
3039                this=exp.Paren(
3040                    this=exp.Mod(
3041                        this=exp.Abs(this=exp.Anonymous(this="HASH", expressions=[gen])),
3042                        expression=exp.Literal.number(1000000),
3043                    )
3044                ),
3045                expression=exp.Literal.number(1000000.0),
3046            )
3047        else:
3048            random_expr = exp.Rand()
3049
3050        # Build: min + random * (max - min [+ 1 for int])
3051        range_expr: exp.Expr = exp.Sub(this=max_val, expression=min_val)
3052        if is_int_result:
3053            range_expr = exp.Add(this=range_expr, expression=exp.Literal.number(1))
3054
3055        result: exp.Expr = exp.Add(
3056            this=min_val,
3057            expression=exp.Mul(this=random_expr, expression=exp.Paren(this=range_expr)),
3058        )
3059
3060        if is_int_result:
3061            result = exp.Cast(this=exp.Floor(this=result), to=exp.DType.BIGINT.into_expr())
3062
3063        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:
3065    def timefromparts_sql(self, expression: exp.TimeFromParts) -> str:
3066        nano = expression.args.get("nano")
3067        overflow = expression.args.get("overflow")
3068
3069        # Snowflake's TIME_FROM_PARTS supports overflow
3070        if overflow:
3071            hour = expression.args["hour"]
3072            minute = expression.args["min"]
3073            sec = expression.args["sec"]
3074
3075            # Check if values are within normal ranges - use MAKE_TIME for efficiency
3076            if not nano and all(arg.is_int for arg in [hour, minute, sec]):
3077                try:
3078                    h_val = hour.to_py()
3079                    m_val = minute.to_py()
3080                    s_val = sec.to_py()
3081                    if 0 <= h_val <= 23 and 0 <= m_val <= 59 and 0 <= s_val <= 59:
3082                        return rename_func("MAKE_TIME")(self, expression)
3083                except ValueError:
3084                    pass
3085
3086            # Overflow or nanoseconds detected - use INTERVAL arithmetic
3087            if nano:
3088                sec = sec + nano.pop() / exp.Literal.number(1000000000.0)
3089
3090            total_seconds = hour * exp.Literal.number(3600) + minute * exp.Literal.number(60) + sec
3091
3092            return self.sql(
3093                exp.Add(
3094                    this=exp.Cast(
3095                        this=exp.Literal.string("00:00:00"), to=exp.DType.TIME.into_expr()
3096                    ),
3097                    expression=exp.Interval(this=total_seconds, unit=exp.var("SECOND")),
3098                )
3099            )
3100
3101        # Default: MAKE_TIME
3102        if nano:
3103            expression.set(
3104                "sec", expression.args["sec"] + nano.pop() / exp.Literal.number(1000000000.0)
3105            )
3106
3107        return rename_func("MAKE_TIME")(self, expression)
def extract_sql(self, expression: sqlglot.expressions.temporal.Extract) -> str:
3109    def extract_sql(self, expression: exp.Extract) -> str:
3110        """
3111        Transpile EXTRACT/DATE_PART for DuckDB, handling specifiers not natively supported.
3112
3113        DuckDB doesn't support: WEEKISO, YEAROFWEEK, YEAROFWEEKISO, NANOSECOND,
3114        EPOCH_SECOND (as integer), EPOCH_MILLISECOND, EPOCH_MICROSECOND, EPOCH_NANOSECOND
3115        """
3116        this = expression.this
3117        datetime_expr = expression.expression
3118
3119        # TIMESTAMPTZ extractions may produce different results between Snowflake and DuckDB
3120        # because Snowflake applies server timezone while DuckDB uses local timezone
3121        if datetime_expr.is_type(exp.DType.TIMESTAMPTZ, exp.DType.TIMESTAMPLTZ):
3122            self.unsupported(
3123                "EXTRACT from TIMESTAMPTZ / TIMESTAMPLTZ may produce different results due to timezone handling differences"
3124            )
3125
3126        part_name = this.name.upper()
3127
3128        if part_name in self.EXTRACT_STRFTIME_MAPPINGS:
3129            fmt, cast_type = self.EXTRACT_STRFTIME_MAPPINGS[part_name]
3130
3131            # Problem: strftime doesn't accept TIME and there's no NANOSECOND function
3132            # So, for NANOSECOND with TIME, fallback to MICROSECOND * 1000
3133            is_nano_time = part_name == "NANOSECOND" and datetime_expr.is_type(
3134                exp.DType.TIME, exp.DType.TIMETZ
3135            )
3136
3137            if is_nano_time:
3138                self.unsupported("Parameter NANOSECOND is not supported with TIME type in DuckDB")
3139                return self.sql(
3140                    exp.cast(
3141                        exp.Mul(
3142                            this=exp.Extract(this=exp.var("MICROSECOND"), expression=datetime_expr),
3143                            expression=exp.Literal.number(1000),
3144                        ),
3145                        exp.DataType.from_str(cast_type, dialect="duckdb"),
3146                    )
3147                )
3148
3149            # For NANOSECOND, cast to TIMESTAMP_NS to preserve nanosecond precision
3150            strftime_input = datetime_expr
3151            if part_name == "NANOSECOND":
3152                strftime_input = exp.cast(datetime_expr, exp.DType.TIMESTAMP_NS)
3153
3154            return self.sql(
3155                exp.cast(
3156                    exp.Anonymous(
3157                        this="STRFTIME",
3158                        expressions=[strftime_input, exp.Literal.string(fmt)],
3159                    ),
3160                    exp.DataType.from_str(cast_type, dialect="duckdb"),
3161                )
3162            )
3163
3164        if part_name in self.EXTRACT_EPOCH_MAPPINGS:
3165            func_name = self.EXTRACT_EPOCH_MAPPINGS[part_name]
3166            result: exp.Expr = exp.Anonymous(this=func_name, expressions=[datetime_expr])
3167            # EPOCH returns float, cast to BIGINT for integer result
3168            if part_name == "EPOCH_SECOND":
3169                result = exp.cast(result, exp.DataType.from_str("BIGINT", dialect="duckdb"))
3170            return self.sql(result)
3171
3172        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:
3174    def timestampfromparts_sql(self, expression: exp.TimestampFromParts) -> str:
3175        # Check if this is the date/time expression form: TIMESTAMP_FROM_PARTS(date_expr, time_expr)
3176        date_expr = expression.this
3177        time_expr = expression.expression
3178
3179        if date_expr is not None and time_expr is not None:
3180            # In DuckDB, DATE + TIME produces TIMESTAMP
3181            return self.sql(exp.Add(this=date_expr, expression=time_expr))
3182
3183        # Component-based form: TIMESTAMP_FROM_PARTS(year, month, day, hour, minute, second, ...)
3184        sec = expression.args.get("sec")
3185        if sec is None:
3186            # This shouldn't happen with valid input, but handle gracefully
3187            return rename_func("MAKE_TIMESTAMP")(self, expression)
3188
3189        milli = expression.args.get("milli")
3190        if milli is not None:
3191            sec += milli.pop() / exp.Literal.number(1000.0)
3192
3193        nano = expression.args.get("nano")
3194        if nano is not None:
3195            sec += nano.pop() / exp.Literal.number(1000000000.0)
3196
3197        if milli or nano:
3198            expression.set("sec", sec)
3199
3200        if zone := expression.args.get("zone"):
3201            return self.sql(exp.AtTimeZone(this=expression, zone=zone.pop()))
3202
3203        return rename_func("MAKE_TIMESTAMP")(self, expression)
@unsupported_args('nano')
def timestampltzfromparts_sql( self, expression: sqlglot.expressions.temporal.TimestampLtzFromParts) -> str:
3205    @unsupported_args("nano")
3206    def timestampltzfromparts_sql(self, expression: exp.TimestampLtzFromParts) -> str:
3207        # Pop nano so rename_func only passes args that MAKE_TIMESTAMP accepts
3208        if nano := expression.args.get("nano"):
3209            nano.pop()
3210
3211        timestamp = rename_func("MAKE_TIMESTAMP")(self, expression)
3212        return f"CAST({timestamp} AS TIMESTAMPTZ)"
@unsupported_args('nano')
def timestamptzfromparts_sql( self, expression: sqlglot.expressions.temporal.TimestampTzFromParts) -> str:
3214    @unsupported_args("nano")
3215    def timestamptzfromparts_sql(self, expression: exp.TimestampTzFromParts) -> str:
3216        # Extract zone before popping
3217        zone = expression.args.get("zone")
3218        # Pop zone and nano so rename_func only passes args that MAKE_TIMESTAMP accepts
3219        if zone:
3220            zone = zone.pop()
3221
3222        if nano := expression.args.get("nano"):
3223            nano.pop()
3224
3225        timestamp = rename_func("MAKE_TIMESTAMP")(self, expression)
3226
3227        if zone:
3228            # Use AT TIME ZONE to apply the explicit timezone
3229            return f"{timestamp} AT TIME ZONE {self.sql(zone)}"
3230
3231        return timestamp
def after_limit_modifiers(self, expression: sqlglot.expressions.core.Expr) -> list[str]:
3233    def after_limit_modifiers(self, expression: exp.Expr) -> list[str]:
3234        locks = self.expressions(expression, key="locks", sep=" ")
3235        return [f" {locks}" if locks else ""]
def tablesample_sql( self, expression: sqlglot.expressions.query.TableSample, tablesample_keyword: str | None = None) -> str:
3237    def tablesample_sql(
3238        self,
3239        expression: exp.TableSample,
3240        tablesample_keyword: str | None = None,
3241    ) -> str:
3242        if not isinstance(expression.parent, exp.Select):
3243            # This sample clause only applies to a single source, not the entire resulting relation
3244            tablesample_keyword = "TABLESAMPLE"
3245
3246        if expression.args.get("size"):
3247            method = expression.args.get("method")
3248            if method and method.name.upper() != "RESERVOIR":
3249                self.unsupported(
3250                    f"Sampling method {method} is not supported with a discrete sample count, "
3251                    "defaulting to reservoir sampling"
3252                )
3253                expression.set("method", exp.var("RESERVOIR"))
3254
3255        return super().tablesample_sql(expression, tablesample_keyword=tablesample_keyword)
def in_sql(self, expression: sqlglot.expressions.core.In) -> str:
3257    def in_sql(self, expression: exp.In) -> str:
3258        unnest = expression.args.get("unnest")
3259        if unnest:
3260            return self.sql(
3261                exp.replace_placeholders(
3262                    self.IN_UNNEST_TEMPLATE, arr=unnest.expressions[0], value=expression.this
3263                )
3264            )
3265        return super().in_sql(expression)
def interval_sql(self, expression: sqlglot.expressions.datatypes.Interval) -> str:
3267    def interval_sql(self, expression: exp.Interval) -> str:
3268        # DuckDB truncates INTERVAL '1.5' DAY to 1 day, but keeps the fraction in INTERVAL '1.5 DAY'
3269        this = expression.this
3270        unit = expression.unit
3271        if expression.args.get("fraction_in_string") and this and isinstance(unit, exp.Var):
3272            return f"INTERVAL '{this.name} {self.sql(unit)}'"
3273
3274        return super().interval_sql(expression)
def join_sql( self, expression: sqlglot.expressions.query.Join, join_op: str | None = None) -> str:
3276    def join_sql(self, expression: exp.Join, join_op: str | None = None) -> str:
3277        if expression.kind in ("MARK", "SINGLE") or (
3278            expression.side == "RIGHT" and expression.is_semi_or_anti_join
3279        ):
3280            kind = "_".join(part for part in (expression.side, expression.kind) if part)
3281            return super().join_sql(expression, join_op=f"JOIN BY (TYPE {kind})")
3282
3283        if (
3284            not expression.args.get("using")
3285            and not expression.args.get("on")
3286            and not expression.args.get("nearest")
3287            and not expression.method
3288            and (expression.kind in ("", "INNER", "OUTER"))
3289        ):
3290            # Some dialects support `LEFT/INNER JOIN UNNEST(...)` without an explicit ON clause
3291            # DuckDB doesn't, but we can just add a dummy ON clause that is always true
3292            if isinstance(expression.this, exp.Unnest):
3293                return super().join_sql(expression.on(exp.true()))
3294
3295            expression.set("side", None)
3296            expression.set("kind", None)
3297
3298        return super().join_sql(expression, join_op=join_op)
def countif_sql(self, expression: sqlglot.expressions.aggregate.CountIf) -> str:
3300    def countif_sql(self, expression: exp.CountIf) -> str:
3301        if self.dialect.version >= (1, 2):
3302            this = expression.this
3303            if expression.args.get("zero_on_all_null") and not isinstance(this, exp.Distinct):
3304                # DuckDB >= 1.2's COUNT_IF returns NULL when the condition is NULL on all rows,
3305                # so we wrap the condition in IS TRUE to preserve count-like semantics
3306                expression = exp.CountIf(this=exp.paren(this).is_(exp.true()))
3307            return self.function_fallback_sql(expression)
3308
3309        # https://github.com/tobymao/sqlglot/pull/4749
3310        return count_if_to_sum(self, expression)
def bracket_sql(self, expression: sqlglot.expressions.core.Bracket) -> str:
3312    def bracket_sql(self, expression: exp.Bracket) -> str:
3313        if self.dialect.version >= (1, 2):
3314            return super().bracket_sql(expression)
3315
3316        # https://duckdb.org/2025/02/05/announcing-duckdb-120.html#breaking-changes
3317        this = expression.this
3318        if isinstance(this, exp.Array):
3319            this.replace(exp.paren(this))
3320
3321        bracket = super().bracket_sql(expression)
3322
3323        if not expression.args.get("returns_list_for_maps"):
3324            if not this.type:
3325                from sqlglot.optimizer.annotate_types import annotate_types
3326
3327                this = annotate_types(this, dialect=self.dialect)
3328
3329            if this.is_type(exp.DType.MAP):
3330                bracket = f"({bracket})[1]"
3331
3332        return bracket
def withingroup_sql(self, expression: sqlglot.expressions.core.WithinGroup) -> str:
3334    def withingroup_sql(self, expression: exp.WithinGroup) -> str:
3335        func = expression.this
3336
3337        # For ARRAY_AGG, DuckDB requires ORDER BY inside the function, not in WITHIN GROUP
3338        # Transform: ARRAY_AGG(x) WITHIN GROUP (ORDER BY y) -> ARRAY_AGG(x ORDER BY y)
3339        if isinstance(func, exp.ArrayAgg):
3340            if not isinstance(order := expression.expression, exp.Order):
3341                return self.sql(func)
3342
3343            # Save the original column for FILTER clause (before wrapping with Order)
3344            original_this = func.this
3345
3346            # Move ORDER BY inside ARRAY_AGG by wrapping its argument with Order
3347            # ArrayAgg.this should become Order(this=ArrayAgg.this, expressions=order.expressions)
3348            func.set(
3349                "this",
3350                exp.Order(
3351                    this=func.this.copy(),
3352                    expressions=order.expressions,
3353                ),
3354            )
3355
3356            # Generate the ARRAY_AGG function with ORDER BY and add FILTER clause if needed
3357            # Use original_this (not the Order-wrapped version) for the FILTER condition
3358            array_agg_sql = self.function_fallback_sql(func)
3359            return self._add_arrayagg_null_filter(array_agg_sql, func, original_this)
3360
3361        # For other functions (like PERCENTILES), use existing logic
3362        expression_sql = self.sql(expression, "expression")
3363
3364        if isinstance(func, exp.PERCENTILES):
3365            # Make the order key the first arg and slide the fraction to the right
3366            # https://duckdb.org/docs/sql/aggregates#ordered-set-aggregate-functions
3367            order_col = expression.find(exp.Ordered)
3368            if order_col:
3369                func.set("expression", func.this)
3370                func.set("this", order_col.this)
3371
3372        this = self.sql(expression, "this").rstrip(")")
3373
3374        return f"{this}{expression_sql})"
def length_sql(self, expression: sqlglot.expressions.string.Length) -> str:
3376    def length_sql(self, expression: exp.Length) -> str:
3377        arg = expression.this
3378
3379        # Dialects like BQ and Snowflake also accept binary values as args, so
3380        # DDB will attempt to infer the type or resort to case/when resolution
3381        if not expression.args.get("binary") or arg.is_string:
3382            return self.func("LENGTH", arg)
3383
3384        if not arg.type:
3385            from sqlglot.optimizer.annotate_types import annotate_types
3386
3387            arg = annotate_types(arg, dialect=self.dialect)
3388
3389        if arg.is_type(*exp.DataType.TEXT_TYPES):
3390            return self.func("LENGTH", arg)
3391
3392        # We need these casts to make duckdb's static type checker happy
3393        blob = exp.cast(arg, exp.DType.VARBINARY)
3394        varchar = exp.cast(arg, exp.DType.VARCHAR)
3395
3396        case = (
3397            exp.case(exp.Anonymous(this="TYPEOF", expressions=[arg]))
3398            .when(exp.Literal.string("BLOB"), exp.ByteLength(this=blob))
3399            .else_(exp.Anonymous(this="LENGTH", expressions=[varchar]))
3400        )
3401        return self.sql(case)
def bitlength_sql(self, expression: sqlglot.expressions.string.BitLength) -> str:
3403    def bitlength_sql(self, expression: exp.BitLength) -> str:
3404        if not _is_binary(arg := expression.this):
3405            return self.func("BIT_LENGTH", arg)
3406
3407        blob = exp.cast(arg, exp.DataType.Type.VARBINARY)
3408        return self.sql(exp.ByteLength(this=blob) * exp.Literal.number(8))
def chr_sql( self, expression: sqlglot.expressions.string.Chr, name: str = 'CHR') -> str:
3410    def chr_sql(self, expression: exp.Chr, name: str = "CHR") -> str:
3411        arg = expression.expressions[0]
3412        if arg.is_type(*exp.DataType.REAL_TYPES):
3413            arg = exp.cast(arg, exp.DType.INT)
3414        return self.func("CHR", arg)
def collation_sql(self, expression: sqlglot.expressions.functions.Collation) -> str:
3416    def collation_sql(self, expression: exp.Collation) -> str:
3417        self.unsupported("COLLATION function is not supported by DuckDB")
3418        return self.function_fallback_sql(expression)
def collate_sql(self, expression: sqlglot.expressions.functions.Collate) -> str:
3420    def collate_sql(self, expression: exp.Collate) -> str:
3421        if not expression.expression.is_string:
3422            return super().collate_sql(expression)
3423
3424        raw = expression.expression.name
3425        if not raw:
3426            return self.sql(expression.this)
3427
3428        parts = []
3429        for part in raw.split("-"):
3430            lower = part.lower()
3431            if lower not in _SNOWFLAKE_COLLATION_DEFAULTS:
3432                if lower in _SNOWFLAKE_COLLATION_UNSUPPORTED:
3433                    self.unsupported(
3434                        f"Snowflake collation specifier '{part}' has no DuckDB equivalent"
3435                    )
3436                parts.append(lower)
3437
3438        if not parts:
3439            return self.sql(expression.this)
3440        return super().collate_sql(
3441            exp.Collate(this=expression.this, expression=exp.var(".".join(parts)))
3442        )
def regexpcount_sql(self, expression: sqlglot.expressions.string.RegexpCount) -> str:
3474    def regexpcount_sql(self, expression: exp.RegexpCount) -> str:
3475        this = expression.this
3476        pattern = expression.expression
3477        position = expression.args.get("position")
3478        parameters = expression.args.get("parameters")
3479
3480        # Validate flags - only "ims" flags are supported for embedded patterns
3481        validated_flags = self._validate_regexp_flags(parameters, supported_flags="ims")
3482
3483        if position:
3484            this = exp.Substring(this=this, start=position)
3485
3486        # Handle empty pattern: Snowflake returns 0, DuckDB would match between every character.
3487        # Checked before the flags are embedded, which would make the pattern non-empty
3488        is_empty = exp.EQ(this=pattern.copy(), expression=exp.Literal.string(""))
3489
3490        # Embed flags in pattern (REGEXP_EXTRACT_ALL doesn't support flags argument)
3491        if validated_flags:
3492            pattern = exp.Concat(expressions=[exp.Literal.string(f"(?{validated_flags})"), pattern])
3493
3494        result = (
3495            exp.case()
3496            .when(is_empty, exp.Literal.number(0))
3497            .else_(
3498                exp.Length(
3499                    this=exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern])
3500                )
3501            )
3502        )
3503
3504        return self.sql(result)
def regexpreplace_sql(self, expression: sqlglot.expressions.string.RegexpReplace) -> str:
3506    def regexpreplace_sql(self, expression: exp.RegexpReplace) -> str:
3507        subject = expression.this
3508        pattern = expression.expression
3509        replacement = expression.args.get("replacement") or exp.Literal.string("")
3510        position = expression.args.get("position")
3511        occurrence = expression.args.get("occurrence")
3512        modifiers = expression.args.get("modifiers")
3513
3514        validated_flags = self._validate_regexp_flags(modifiers, supported_flags="cimsg") or ""
3515
3516        # Handle occurrence (only literals supported)
3517        if occurrence and not occurrence.is_int:
3518            self.unsupported("REGEXP_REPLACE with non-literal occurrence")
3519        else:
3520            occurrence = occurrence.to_py() if occurrence and occurrence.is_int else 0
3521            if occurrence > 1:
3522                self.unsupported(f"REGEXP_REPLACE occurrence={occurrence} not supported")
3523            # flag duckdb to do either all or none, single_replace check is for duckdb round trip
3524            elif (
3525                occurrence == 0
3526                and "g" not in validated_flags
3527                and not expression.args.get("single_replace")
3528            ):
3529                validated_flags += "g"
3530
3531        # Handle position (only literals supported)
3532        prefix = None
3533        if position and not position.is_int:
3534            self.unsupported("REGEXP_REPLACE with non-literal position")
3535        elif position and position.is_int and position.to_py() > 1:
3536            pos = position.to_py()
3537            prefix = exp.Substring(
3538                this=subject, start=exp.Literal.number(1), length=exp.Literal.number(pos - 1)
3539            )
3540            subject = exp.Substring(this=subject, start=exp.Literal.number(pos))
3541
3542        result: exp.Expr = exp.Anonymous(
3543            this="REGEXP_REPLACE",
3544            expressions=[
3545                subject,
3546                pattern,
3547                replacement,
3548                exp.Literal.string(validated_flags) if validated_flags else None,
3549            ],
3550        )
3551
3552        if prefix:
3553            result = exp.Concat(expressions=[prefix, result])
3554
3555        return self.sql(result)
def regexplike_sql(self, expression: sqlglot.expressions.core.RegexpLike) -> str:
3557    def regexplike_sql(self, expression: exp.RegexpLike) -> str:
3558        this = expression.this
3559        pattern = expression.expression
3560        flag = expression.args.get("flag")
3561
3562        if expression.args.get("full_match"):
3563            validated_flags = self._validate_regexp_flags(flag, supported_flags="cims")
3564            flag = exp.Literal.string(validated_flags) if validated_flags else None
3565            return self.func("REGEXP_FULL_MATCH", this, pattern, flag)
3566
3567        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:
3569    @unsupported_args("ins_cost", "del_cost", "sub_cost")
3570    def levenshtein_sql(self, expression: exp.Levenshtein) -> str:
3571        this = expression.this
3572        expr = expression.expression
3573        max_dist = expression.args.get("max_dist")
3574
3575        if max_dist is None:
3576            return self.func("LEVENSHTEIN", this, expr)
3577
3578        # Emulate Snowflake semantics: if distance > max_dist, return max_dist
3579        levenshtein = exp.Levenshtein(this=this, expression=expr)
3580        return self.sql(exp.Least(this=levenshtein, expressions=[max_dist]))
def pad_sql(self, expression: sqlglot.expressions.string.Pad) -> str:
3582    def pad_sql(self, expression: exp.Pad) -> str:
3583        """
3584        Handle RPAD/LPAD for VARCHAR and BINARY types.
3585
3586        For VARCHAR: Delegate to parent class
3587        For BINARY: Lower to: input || REPEAT(pad, GREATEST(0, target_len - OCTET_LENGTH(input)))
3588        """
3589        string_arg = expression.this
3590        fill_arg = expression.args.get("fill_pattern") or exp.Literal.string(" ")
3591
3592        if _is_binary(string_arg) or _is_binary(fill_arg):
3593            length_arg = expression.expression
3594            is_left = expression.args.get("is_left")
3595
3596            input_len = exp.ByteLength(this=string_arg)
3597            chars_needed = length_arg - input_len
3598            pad_count = exp.Greatest(
3599                this=exp.Literal.number(0), expressions=[chars_needed], ignore_nulls=True
3600            )
3601            repeat_expr = exp.Repeat(this=fill_arg, times=pad_count)
3602
3603            left, right = string_arg, repeat_expr
3604            if is_left:
3605                left, right = right, left
3606
3607            result = exp.DPipe(this=left, expression=right)
3608            return self.sql(result)
3609
3610        # For VARCHAR: Delegate to parent class (handles PAD_FILL_PATTERN_IS_REQUIRED)
3611        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:
3613    def minhash_sql(self, expression: exp.Minhash) -> str:
3614        k = expression.this
3615        exprs = expression.expressions
3616
3617        if len(exprs) != 1 or isinstance(exprs[0], exp.Star):
3618            self.unsupported(
3619                "MINHASH with multiple expressions or * requires manual query restructuring"
3620            )
3621            return self.func("MINHASH", k, *exprs)
3622
3623        expr = exprs[0]
3624        result = exp.replace_placeholders(self.MINHASH_TEMPLATE.copy(), expr=expr, k=k)
3625        return f"({self.sql(result)})"
def minhashcombine_sql(self, expression: sqlglot.expressions.aggregate.MinhashCombine) -> str:
3627    def minhashcombine_sql(self, expression: exp.MinhashCombine) -> str:
3628        expr = expression.this
3629        result = exp.replace_placeholders(self.MINHASH_COMBINE_TEMPLATE.copy(), expr=expr)
3630        return f"({self.sql(result)})"
def approximatesimilarity_sql( self, expression: sqlglot.expressions.aggregate.ApproximateSimilarity) -> str:
3632    def approximatesimilarity_sql(self, expression: exp.ApproximateSimilarity) -> str:
3633        expr = expression.this
3634        result = exp.replace_placeholders(self.APPROXIMATE_SIMILARITY_TEMPLATE.copy(), expr=expr)
3635        return f"({self.sql(result)})"
def arrayuniqueagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayUniqueAgg) -> str:
3637    def arrayuniqueagg_sql(self, expression: exp.ArrayUniqueAgg) -> str:
3638        return self.sql(
3639            exp.Filter(
3640                this=exp.func("LIST", exp.Distinct(expressions=[expression.this])),
3641                expression=exp.Where(this=expression.this.copy().is_(exp.null()).not_()),
3642            )
3643        )
def arrayconcatagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayConcatAgg) -> str:
3645    def arrayconcatagg_sql(self, expression: exp.ArrayConcatAgg) -> str:
3646        this = expression.this
3647
3648        if isinstance(this, exp.Limit):
3649            self.unsupported("LIMIT in ARRAY_CONCAT_AGG cannot be transpiled to DuckDB")
3650            this = this.this
3651
3652        inner = this.this if isinstance(this, exp.Order) else this
3653
3654        return self.func(
3655            "FLATTEN",
3656            exp.Filter(
3657                this=exp.ArrayAgg(this=this),
3658                expression=exp.Where(this=inner.copy().is_(exp.null()).not_()),
3659            ),
3660        )
def arrayunionagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayUnionAgg) -> str:
3662    def arrayunionagg_sql(self, expression: exp.ArrayUnionAgg) -> str:
3663        self.unsupported("ARRAY_UNION_AGG is not supported in DuckDB")
3664        return self.function_fallback_sql(expression)
def arraydistinct_sql(self, expression: sqlglot.expressions.array.ArrayDistinct) -> str:
3666    def arraydistinct_sql(self, expression: exp.ArrayDistinct) -> str:
3667        arr = expression.this
3668        func = self.func("LIST_DISTINCT", arr)
3669
3670        if expression.args.get("check_null"):
3671            add_null_to_array = exp.func(
3672                "LIST_APPEND", exp.func("LIST_DISTINCT", exp.ArrayCompact(this=arr)), exp.Null()
3673            )
3674            return self.sql(
3675                exp.If(
3676                    this=exp.NEQ(
3677                        this=exp.ArraySize(this=arr), expression=exp.func("LIST_COUNT", arr)
3678                    ),
3679                    true=add_null_to_array,
3680                    false=func,
3681                )
3682            )
3683
3684        return func
def arrayintersect_sql(self, expression: sqlglot.expressions.array.ArrayIntersect) -> str:
3686    def arrayintersect_sql(self, expression: exp.ArrayIntersect) -> str:
3687        if expression.args.get("is_multiset") and len(expression.expressions) == 2:
3688            return self._array_bag_sql(
3689                self.ARRAY_INTERSECTION_CONDITION,
3690                expression.expressions[0],
3691                expression.expressions[1],
3692            )
3693        return self.function_fallback_sql(expression)
def arrayexcept_sql(self, expression: sqlglot.expressions.array.ArrayExcept) -> str:
3695    def arrayexcept_sql(self, expression: exp.ArrayExcept) -> str:
3696        arr1, arr2 = expression.this, expression.expression
3697        if expression.args.get("is_multiset"):
3698            return self._array_bag_sql(self.ARRAY_EXCEPT_CONDITION, arr1, arr2)
3699        return self.sql(
3700            exp.replace_placeholders(self.ARRAY_EXCEPT_SET_TEMPLATE, arr1=arr1, arr2=arr2)
3701        )
def arrayslice_sql(self, expression: sqlglot.expressions.array.ArraySlice) -> str:
3703    def arrayslice_sql(self, expression: exp.ArraySlice) -> str:
3704        """
3705        Spark's SLICE uses a length, so normalize negative starts before computing
3706        the inclusive end index. Starts before the array produce an empty slice.
3707
3708        Transpiles Snowflake's ARRAY_SLICE (0-indexed, exclusive end) to DuckDB's
3709        ARRAY_SLICE (1-indexed, inclusive end) by wrapping start and end in CASE
3710        expressions that adjust the index at query time:
3711          - start: CASE WHEN start >= 0 THEN start + 1 ELSE start END
3712          - end:   CASE WHEN end < 0 THEN end - 1 ELSE end END
3713        """
3714        start, end = expression.args.get("start"), expression.args.get("end")
3715
3716        if expression.args.get("zero_based"):
3717            if start is not None:
3718                start = (
3719                    exp.case()
3720                    .when(
3721                        exp.GTE(this=start.copy(), expression=exp.Literal.number(0)),
3722                        exp.Add(this=start.copy(), expression=exp.Literal.number(1)),
3723                    )
3724                    .else_(start)
3725                )
3726            if end is not None and not expression.args.get("is_length"):
3727                end = (
3728                    exp.case()
3729                    .when(
3730                        exp.LT(this=end.copy(), expression=exp.Literal.number(0)),
3731                        exp.Sub(this=end.copy(), expression=exp.Literal.number(1)),
3732                    )
3733                    .else_(end)
3734                )
3735
3736        if expression.args.get("is_length") and start is not None and end is not None:
3737            if not start.is_int or start.to_py() < 0:
3738                size = exp.ArraySize(this=expression.this.copy())
3739                start = (
3740                    exp.case()
3741                    .when(start < -size, size + 1)
3742                    .when(start < 0, size + start + 1)
3743                    .else_(start)
3744                )
3745
3746            if start.is_int and end.is_int:
3747                end = exp.Literal.number(start.to_py() + end.to_py() - 1)
3748            else:
3749                end = start + exp.cast(end, "BIGINT") - 1
3750        return self.func("ARRAY_SLICE", expression.this, start, end, expression.args.get("step"))

Spark's SLICE uses a length, so normalize negative starts before computing the inclusive end index. Starts before the array produce an empty slice.

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:
3752    def arrayszip_sql(self, expression: exp.ArraysZip) -> str:
3753        args = expression.expressions
3754
3755        if not args:
3756            # Return [{}] - using MAP([], []) since DuckDB can't represent empty structs
3757            return self.sql(exp.array(exp.Map(keys=exp.array(), values=exp.array())))
3758
3759        # Build placeholder values for template
3760        lengths = [exp.Length(this=arg) for arg in args]
3761        max_len = (
3762            lengths[0]
3763            if len(lengths) == 1
3764            else exp.Greatest(this=lengths[0], expressions=lengths[1:])
3765        )
3766
3767        # Empty struct with same schema: {'$1': NULL, '$2': NULL, ...}
3768        empty_struct = exp.func(
3769            "STRUCT",
3770            *[
3771                exp.PropertyEQ(this=exp.Literal.string(f"${i + 1}"), expression=exp.Null())
3772                for i in range(len(args))
3773            ],
3774        )
3775
3776        # Struct for transform: {'$1': COALESCE(arr1, [])[__i + 1], ...}
3777        # COALESCE wrapping handles NULL arrays - prevents invalid NULL[i] syntax
3778        index = exp.column("__i") + 1
3779        transform_struct = exp.func(
3780            "STRUCT",
3781            *[
3782                exp.PropertyEQ(
3783                    this=exp.Literal.string(f"${i + 1}"),
3784                    expression=exp.func("COALESCE", arg, exp.array())[index],
3785                )
3786                for i, arg in enumerate(args)
3787            ],
3788        )
3789
3790        result = exp.replace_placeholders(
3791            self.ARRAYS_ZIP_TEMPLATE.copy(),
3792            null_check=exp.or_(*[arg.is_(exp.Null()) for arg in args]),
3793            all_empty_check=exp.and_(
3794                *[
3795                    exp.EQ(this=exp.Length(this=arg), expression=exp.Literal.number(0))
3796                    for arg in args
3797                ]
3798            ),
3799            empty_struct=empty_struct,
3800            max_len=max_len,
3801            transform_struct=transform_struct,
3802        )
3803        return self.sql(result)
def lower_sql(self, expression: sqlglot.expressions.string.Lower) -> str:
3805    def lower_sql(self, expression: exp.Lower) -> str:
3806        result_sql = self.func("LOWER", _cast_to_varchar(expression.this))
3807        return _gen_with_cast_to_blob(self, expression, result_sql)
def upper_sql(self, expression: sqlglot.expressions.string.Upper) -> str:
3809    def upper_sql(self, expression: exp.Upper) -> str:
3810        result_sql = self.func("UPPER", _cast_to_varchar(expression.this))
3811        return _gen_with_cast_to_blob(self, expression, result_sql)
def reverse_sql(self, expression: sqlglot.expressions.string.Reverse) -> str:
3813    def reverse_sql(self, expression: exp.Reverse) -> str:
3814        result_sql = self.func("REVERSE", _cast_to_varchar(expression.this))
3815        return _gen_with_cast_to_blob(self, expression, result_sql)
def left_sql(self, expression: sqlglot.expressions.string.Left) -> str:
3841    def left_sql(self, expression: exp.Left) -> str:
3842        return self._left_right_sql(expression, "LEFT")
def right_sql(self, expression: sqlglot.expressions.string.Right) -> str:
3844    def right_sql(self, expression: exp.Right) -> str:
3845        return self._left_right_sql(expression, "RIGHT")
def rtrimmedlength_sql(self, expression: sqlglot.expressions.string.RtrimmedLength) -> str:
3847    def rtrimmedlength_sql(self, expression: exp.RtrimmedLength) -> str:
3848        return self.func("LENGTH", exp.Trim(this=expression.this, position="TRAILING"))
def stuff_sql(self, expression: sqlglot.expressions.string.Stuff) -> str:
3850    def stuff_sql(self, expression: exp.Stuff) -> str:
3851        base = expression.this
3852        start = expression.args["start"]
3853        length = expression.args["length"]
3854        insertion = expression.expression
3855        is_binary = _is_binary(base)
3856
3857        if is_binary:
3858            # DuckDB's SUBSTRING doesn't accept BLOB; operate on the HEX string instead
3859            # (each byte = 2 hex chars), then UNHEX back to BLOB
3860            base = exp.Hex(this=base)
3861            insertion = exp.Hex(this=insertion)
3862            left = exp.Substring(
3863                this=base.copy(),
3864                start=exp.Literal.number(1),
3865                length=(start.copy() - exp.Literal.number(1)) * exp.Literal.number(2),
3866            )
3867            right = exp.Substring(
3868                this=base.copy(),
3869                start=((start + length) - exp.Literal.number(1)) * exp.Literal.number(2)
3870                + exp.Literal.number(1),
3871            )
3872        else:
3873            left = exp.Substring(
3874                this=base.copy(),
3875                start=exp.Literal.number(1),
3876                length=start.copy() - exp.Literal.number(1),
3877            )
3878            right = exp.Substring(this=base.copy(), start=start + length)
3879        result: exp.Expr = exp.DPipe(
3880            this=exp.DPipe(this=left, expression=insertion), expression=right
3881        )
3882
3883        if is_binary:
3884            result = exp.Unhex(this=result)
3885
3886        return self.sql(result)
def rand_sql(self, expression: sqlglot.expressions.functions.Rand) -> str:
3888    def rand_sql(self, expression: exp.Rand) -> str:
3889        seed = expression.this
3890        if seed is not None:
3891            self.unsupported("RANDOM with seed is not supported in DuckDB")
3892
3893        lower = expression.args.get("lower")
3894        upper = expression.args.get("upper")
3895
3896        if lower and upper:
3897            # scale DuckDB's [0,1) to the specified range
3898            range_size = exp.paren(upper - lower)
3899            scaled = exp.Add(this=lower, expression=exp.func("random") * range_size)
3900
3901            # For now we assume that if bounds are set, return type is BIGINT. Snowflake/Teradata
3902            result = exp.cast(scaled, exp.DType.BIGINT)
3903            return self.sql(result)
3904
3905        # Default DuckDB behavior - just return RANDOM() as float
3906        return "RANDOM()"
def bytelength_sql(self, expression: sqlglot.expressions.string.ByteLength) -> str:
3908    def bytelength_sql(self, expression: exp.ByteLength) -> str:
3909        arg = expression.this
3910
3911        # Check if it's a text type (handles both literals and annotated expressions)
3912        if arg.is_type(*exp.DataType.TEXT_TYPES):
3913            return self.func("OCTET_LENGTH", exp.Encode(this=arg))
3914
3915        # Default: pass through as-is (conservative for DuckDB, handles binary and unannotated)
3916        return self.func("OCTET_LENGTH", arg)
def base64encode_sql(self, expression: sqlglot.expressions.string.Base64Encode) -> str:
3918    def base64encode_sql(self, expression: exp.Base64Encode) -> str:
3919        # DuckDB TO_BASE64 requires BLOB input
3920        # Snowflake BASE64_ENCODE accepts both VARCHAR and BINARY - for VARCHAR it implicitly
3921        # encodes UTF-8 bytes. We add ENCODE unless the input is a binary type.
3922        result = expression.this
3923
3924        # Check if input is a string type - ENCODE only accepts VARCHAR
3925        if result.is_type(*exp.DataType.TEXT_TYPES):
3926            result = exp.Encode(this=result)
3927
3928        result = exp.ToBase64(this=result)
3929
3930        max_line_length = expression.args.get("max_line_length")
3931        alphabet = expression.args.get("alphabet")
3932
3933        # Handle custom alphabet by replacing standard chars with custom ones
3934        result = _apply_base64_alphabet_replacements(result, alphabet)
3935
3936        # Handle max_line_length by inserting newlines every N characters
3937        line_length = (
3938            t.cast(int, max_line_length.to_py())
3939            if isinstance(max_line_length, exp.Literal) and max_line_length.is_number
3940            else 0
3941        )
3942        if line_length > 0:
3943            newline = exp.Chr(expressions=[exp.Literal.number(10)])
3944            result = exp.Trim(
3945                this=exp.RegexpReplace(
3946                    this=result,
3947                    expression=exp.Literal.string(f"(.{{{line_length}}})"),
3948                    replacement=exp.Concat(expressions=[exp.Literal.string("\\1"), newline.copy()]),
3949                ),
3950                expression=newline,
3951                position="TRAILING",
3952            )
3953
3954        return self.sql(result)
def hex_sql(self, expression: sqlglot.expressions.string.Hex) -> str:
3956    def hex_sql(self, expression: exp.Hex) -> str:
3957        case = expression.args.get("case")
3958
3959        if not case:
3960            return self.func("HEX", expression.this)
3961
3962        hex_expr = exp.Hex(this=expression.this)
3963        return self.sql(
3964            exp.case()
3965            .when(case.is_(exp.null()), exp.null())
3966            .when(case.copy().eq(0), exp.Lower(this=hex_expr.copy()))
3967            .else_(hex_expr)
3968        )
def replace_sql(self, expression: sqlglot.expressions.string.Replace) -> str:
3970    def replace_sql(self, expression: exp.Replace) -> str:
3971        result_sql = self.func(
3972            "REPLACE",
3973            _cast_to_varchar(expression.this),
3974            _cast_to_varchar(expression.expression),
3975            _cast_to_varchar(expression.args.get("replacement")),
3976        )
3977        return _gen_with_cast_to_blob(self, expression, result_sql)
def bitwisexor_sql(self, expression: sqlglot.expressions.core.BitwiseXor) -> str:
3984    def bitwisexor_sql(self, expression: exp.BitwiseXor) -> str:
3985        _prepare_binary_bitwise_args(expression)
3986        result_sql = self.func("XOR", expression.this, expression.expression)
3987        return _gen_with_cast_to_blob(self, expression, result_sql)
def objectinsert_sql(self, expression: sqlglot.expressions.json.ObjectInsert) -> str:
3989    def objectinsert_sql(self, expression: exp.ObjectInsert) -> str:
3990        this = expression.this
3991        key = expression.args.get("key")
3992        key_sql = key.name if isinstance(key, exp.Expr) else ""
3993        value_sql = self.sql(expression, "value")
3994
3995        kv_sql = f"{key_sql} := {value_sql}"
3996
3997        # If the input struct is empty e.g. transpiling OBJECT_INSERT(OBJECT_CONSTRUCT(), key, value) from Snowflake
3998        # then we can generate STRUCT_PACK which will build it since STRUCT_INSERT({}, key := value) is not valid DuckDB
3999        if isinstance(this, exp.Struct) and not this.expressions:
4000            return self.func("STRUCT_PACK", kv_sql)
4001
4002        return self.func("STRUCT_INSERT", this, kv_sql)
def mapcat_sql(self, expression: sqlglot.expressions.array.MapCat) -> str:
4004    def mapcat_sql(self, expression: exp.MapCat) -> str:
4005        result = exp.replace_placeholders(
4006            self.MAPCAT_TEMPLATE.copy(),
4007            map1=expression.this,
4008            map2=expression.expression,
4009        )
4010        return self.sql(result)
def mapcontainskey_sql(self, expression: sqlglot.expressions.array.MapContainsKey) -> str:
4012    def mapcontainskey_sql(self, expression: exp.MapContainsKey) -> str:
4013        return self.func(
4014            "ARRAY_CONTAINS", exp.func("MAP_KEYS", expression.args["key"]), expression.this
4015        )
def mapdelete_sql(self, expression: sqlglot.expressions.array.MapDelete) -> str:
4017    def mapdelete_sql(self, expression: exp.MapDelete) -> str:
4018        map_arg = expression.this
4019        keys_to_delete = expression.expressions
4020
4021        x_dot_key = exp.Dot(this=exp.to_identifier("x"), expression=exp.to_identifier("key"))
4022
4023        lambda_expr = exp.Lambda(
4024            this=exp.In(this=x_dot_key, expressions=keys_to_delete).not_(),
4025            expressions=[exp.to_identifier("x")],
4026        )
4027        result = exp.func(
4028            "MAP_FROM_ENTRIES",
4029            exp.ArrayFilter(this=exp.func("MAP_ENTRIES", map_arg), expression=lambda_expr),
4030        )
4031        return self.sql(result)
def mappick_sql(self, expression: sqlglot.expressions.array.MapPick) -> str:
4033    def mappick_sql(self, expression: exp.MapPick) -> str:
4034        map_arg = expression.this
4035        keys_to_pick = expression.expressions
4036
4037        x_dot_key = exp.Dot(this=exp.to_identifier("x"), expression=exp.to_identifier("key"))
4038
4039        if len(keys_to_pick) == 1 and keys_to_pick[0].is_type(exp.DType.ARRAY):
4040            lambda_expr = exp.Lambda(
4041                this=exp.func("ARRAY_CONTAINS", keys_to_pick[0], x_dot_key),
4042                expressions=[exp.to_identifier("x")],
4043            )
4044        else:
4045            lambda_expr = exp.Lambda(
4046                this=exp.In(this=x_dot_key, expressions=keys_to_pick),
4047                expressions=[exp.to_identifier("x")],
4048            )
4049
4050        result = exp.func(
4051            "MAP_FROM_ENTRIES",
4052            exp.func("LIST_FILTER", exp.func("MAP_ENTRIES", map_arg), lambda_expr),
4053        )
4054        return self.sql(result)
def mapsize_sql(self, expression: sqlglot.expressions.array.MapSize) -> str:
4056    def mapsize_sql(self, expression: exp.MapSize) -> str:
4057        return self.func("CARDINALITY", expression.this)
@unsupported_args('update_flag')
def mapinsert_sql(self, expression: sqlglot.expressions.array.MapInsert) -> str:
4059    @unsupported_args("update_flag")
4060    def mapinsert_sql(self, expression: exp.MapInsert) -> str:
4061        map_arg = expression.this
4062        key = expression.args.get("key")
4063        value = expression.args.get("value")
4064
4065        map_type = map_arg.type
4066
4067        if value is not None:
4068            if map_type and map_type.expressions and len(map_type.expressions) > 1:
4069                # Extract the value type from MAP(key_type, value_type)
4070                value_type = map_type.expressions[1]
4071                # Cast value to match the map's value type to avoid type conflicts
4072                value = exp.cast(value, value_type)
4073            # else: polymorphic MAP case - no type parameters available, use value as-is
4074
4075        # Create a single-entry map for the new key-value pair
4076        new_entry_struct = exp.Struct(expressions=[exp.PropertyEQ(this=key, expression=value)])
4077        new_entry: exp.Expression = exp.ToMap(this=new_entry_struct)
4078
4079        # Use MAP_CONCAT to merge the original map with the new entry
4080        # This automatically handles both insert and update cases
4081        result = exp.func("MAP_CONCAT", map_arg, new_entry)
4082
4083        return self.sql(result)
def startswith_sql(self, expression: sqlglot.expressions.string.StartsWith) -> str:
4085    def startswith_sql(self, expression: exp.StartsWith) -> str:
4086        return self.func(
4087            "STARTS_WITH",
4088            _cast_to_varchar(expression.this),
4089            _cast_to_varchar(expression.expression),
4090        )
def space_sql(self, expression: sqlglot.expressions.string.Space) -> str:
4092    def space_sql(self, expression: exp.Space) -> str:
4093        # DuckDB's REPEAT requires BIGINT for the count parameter
4094        return self.sql(
4095            exp.Repeat(
4096                this=exp.Literal.string(" "),
4097                times=exp.cast(expression.this, exp.DType.BIGINT),
4098            )
4099        )
def tablefromrows_sql(self, expression: sqlglot.expressions.query.TableFromRows) -> str:
4101    def tablefromrows_sql(self, expression: exp.TableFromRows) -> str:
4102        # For GENERATOR, unwrap TABLE() - just emit the Generator (becomes RANGE)
4103        if isinstance(expression.this, exp.Generator):
4104            # Preserve alias, joins, and other table-level args
4105            table = exp.Table(
4106                this=expression.this,
4107                alias=expression.args.get("alias"),
4108                joins=expression.args.get("joins"),
4109            )
4110            return self.sql(table)
4111
4112        return super().tablefromrows_sql(expression)
def unnest_sql(self, expression: sqlglot.expressions.array.Unnest) -> str:
4114    def unnest_sql(self, expression: exp.Unnest) -> str:
4115        explode_array = expression.args.get("explode_array")
4116        if explode_array:
4117            # In BigQuery, UNNESTing a nested array leads to explosion of the top-level array & struct
4118            # This is transpiled to DDB by transforming "FROM UNNEST(...)" to "FROM (SELECT UNNEST(..., max_depth => 2))"
4119            expression.expressions.append(
4120                exp.Kwarg(this=exp.var("max_depth"), expression=exp.Literal.number(2))
4121            )
4122
4123            # If BQ's UNNEST is aliased, we transform it from a column alias to a table alias in DDB
4124            alias = expression.args.get("alias")
4125            if isinstance(alias, exp.TableAlias):
4126                expression.set("alias", None)
4127                if alias.columns:
4128                    alias = exp.TableAlias(this=seq_get(alias.columns, 0))
4129
4130            unnest_sql = super().unnest_sql(expression)
4131            select = exp.Select(expressions=[unnest_sql]).subquery(alias)
4132            return self.sql(select)
4133
4134        return super().unnest_sql(expression)
def arrayagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayAgg) -> str:
4136    def arrayagg_sql(self, expression: exp.ArrayAgg) -> str:
4137        if isinstance(expression.this, exp.Limit):
4138            self.unsupported("LIMIT inside ARRAY_AGG is not supported in DuckDB")
4139
4140        return super().arrayagg_sql(expression)
def ignorenulls_sql(self, expression: sqlglot.expressions.core.IgnoreNulls) -> str:
4142    def ignorenulls_sql(self, expression: exp.IgnoreNulls) -> str:
4143        this = expression.this
4144
4145        if isinstance(this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
4146            # DuckDB should render IGNORE NULLS only for the general-purpose
4147            # window functions that accept it e.g. FIRST_VALUE(... IGNORE NULLS) OVER (...)
4148            return super().ignorenulls_sql(expression)
4149
4150        # For ARRAY_AGG(expr IGNORE NULLS ...), convert IGNORE NULLS to a
4151        # FILTER(WHERE expr IS NOT NULL) clause by setting nulls_excluded on
4152        # the ArrayAgg.  The existing _add_arrayagg_null_filter method will
4153        # emit the FILTER clause during arrayagg_sql / withingroup_sql.
4154        if isinstance(this, exp.ArrayAgg):
4155            this.set("nulls_excluded", True)
4156            return self.sql(this)
4157
4158        if isinstance(this, exp.First):
4159            this = exp.AnyValue(this=this.this)
4160
4161        if not isinstance(this, (exp.AnyValue, exp.ApproxQuantiles)):
4162            self.unsupported("IGNORE NULLS is not supported for non-window functions.")
4163
4164        return self.sql(this)
def split_sql(self, expression: sqlglot.expressions.string.Split) -> str:
4166    def split_sql(self, expression: exp.Split) -> str:
4167        base_func = exp.func("STR_SPLIT", expression.this, expression.expression)
4168
4169        case_expr = exp.case().else_(base_func)
4170        needs_case = False
4171
4172        if expression.args.get("null_returns_null"):
4173            case_expr = case_expr.when(expression.expression.is_(exp.null()), exp.null())
4174            needs_case = True
4175
4176        if expression.args.get("empty_delimiter_returns_whole"):
4177            # When delimiter is empty string, return input string as single array element
4178            array_with_input = exp.array(expression.this)
4179            case_expr = case_expr.when(
4180                expression.expression.eq(exp.Literal.string("")), array_with_input
4181            )
4182            needs_case = True
4183
4184        return self.sql(case_expr if needs_case else base_func)
def splitpart_sql(self, expression: sqlglot.expressions.string.SplitPart) -> str:
4186    def splitpart_sql(self, expression: exp.SplitPart) -> str:
4187        string_arg = expression.this
4188        delimiter_arg = expression.args.get("delimiter")
4189        part_index_arg = expression.args.get("part_index")
4190
4191        if delimiter_arg and part_index_arg:
4192            # Handle Snowflake's "index 0 and 1 both return first element" behavior
4193            if expression.args.get("part_index_zero_as_one"):
4194                # Convert 0 to 1 for compatibility
4195
4196                part_index_arg = exp.Paren(
4197                    this=exp.case()
4198                    .when(part_index_arg.eq(exp.Literal.number("0")), exp.Literal.number("1"))
4199                    .else_(part_index_arg)
4200                )
4201
4202            # Use Anonymous to avoid recursion
4203            base_func_expr: exp.Expr = exp.Anonymous(
4204                this="SPLIT_PART", expressions=[string_arg, delimiter_arg, part_index_arg]
4205            )
4206            needs_case_transform = False
4207            case_expr = exp.case().else_(base_func_expr)
4208
4209            if expression.args.get("empty_delimiter_returns_whole"):
4210                # When delimiter is empty string:
4211                # - Return whole string if part_index is 1 or -1
4212                # - Return empty string otherwise
4213                empty_case = exp.Paren(
4214                    this=exp.case()
4215                    .when(
4216                        exp.or_(
4217                            part_index_arg.eq(exp.Literal.number("1")),
4218                            part_index_arg.eq(exp.Literal.number("-1")),
4219                        ),
4220                        string_arg,
4221                    )
4222                    .else_(exp.Literal.string(""))
4223                )
4224
4225                case_expr = case_expr.when(delimiter_arg.eq(exp.Literal.string("")), empty_case)
4226                needs_case_transform = True
4227
4228            """
4229            Output looks something like this:
4230
4231            CASE
4232            WHEN delimiter is '' THEN
4233                (
4234                    CASE
4235                    WHEN adjusted_part_index = 1 OR adjusted_part_index = -1 THEN input
4236                    ELSE '' END
4237                )
4238            ELSE SPLIT_PART(input, delimiter, adjusted_part_index)
4239            END
4240
4241            """
4242            return self.sql(case_expr if needs_case_transform else base_func_expr)
4243
4244        return self.function_fallback_sql(expression)
def respectnulls_sql(self, expression: sqlglot.expressions.core.RespectNulls) -> str:
4246    def respectnulls_sql(self, expression: exp.RespectNulls) -> str:
4247        if isinstance(expression.this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
4248            # DuckDB should render RESPECT NULLS only for the general-purpose
4249            # window functions that accept it e.g. FIRST_VALUE(... RESPECT NULLS) OVER (...)
4250            return super().respectnulls_sql(expression)
4251
4252        self.unsupported("RESPECT NULLS is not supported for non-window functions.")
4253        return self.sql(expression, "this")
def arraytostring_sql(self, expression: sqlglot.expressions.array.ArrayToString) -> str:
4255    def arraytostring_sql(self, expression: exp.ArrayToString) -> str:
4256        null = expression.args.get("null")
4257
4258        if expression.args.get("null_is_empty"):
4259            x = exp.to_identifier("x")
4260            list_transform = exp.Transform(
4261                this=expression.this.copy(),
4262                expression=exp.Lambda(
4263                    this=exp.Coalesce(
4264                        this=exp.cast(x, "TEXT"), expressions=[exp.Literal.string("")]
4265                    ),
4266                    expressions=[x],
4267                ),
4268            )
4269            array_to_string = exp.ArrayToString(
4270                this=list_transform, expression=expression.expression
4271            )
4272            if expression.args.get("null_delim_is_null"):
4273                return self.sql(
4274                    exp.case()
4275                    .when(expression.expression.copy().is_(exp.null()), exp.null())
4276                    .else_(array_to_string)
4277                )
4278            return self.sql(array_to_string)
4279
4280        if null:
4281            x = exp.to_identifier("x")
4282            return self.sql(
4283                exp.ArrayToString(
4284                    this=exp.Transform(
4285                        this=expression.this,
4286                        expression=exp.Lambda(
4287                            this=exp.Coalesce(this=exp.cast(x, "TEXT"), expressions=[null]),
4288                            expressions=[x],
4289                        ),
4290                    ),
4291                    expression=expression.expression,
4292                )
4293            )
4294
4295        return self.func("ARRAY_TO_STRING", expression.this, expression.expression)
def concatws_sql(self, expression: sqlglot.expressions.string.ConcatWs) -> str:
4297    def concatws_sql(self, expression: exp.ConcatWs) -> str:
4298        # DuckDB-specific: handle binary types using DPipe (||) operator
4299        separator = seq_get(expression.expressions, 0)
4300        args = expression.expressions[1:]
4301
4302        if any(_is_binary(arg) for arg in [separator, *args]):
4303            result = args[0]
4304            for arg in args[1:]:
4305                result = exp.DPipe(
4306                    this=exp.DPipe(this=result, expression=separator), expression=arg
4307                )
4308            return self.sql(result)
4309
4310        if expression.args.get("flatten") and any(
4311            isinstance(arg, exp.Array) or arg.is_type(exp.DType.ARRAY) for arg in args
4312        ):
4313            arrays = []
4314            for arg in args:
4315                if isinstance(arg, exp.Array) or arg.is_type(exp.DType.ARRAY):
4316                    arg = exp.cast(arg, "TEXT[]")
4317                else:
4318                    if (not arg.type or arg.is_type(exp.DType.UNKNOWN)) and not isinstance(
4319                        arg, exp.CONSTANTS
4320                    ):
4321                        self.unsupported(
4322                            "Cannot transpile CONCAT_WS with unknown argument types to DuckDB."
4323                        )
4324                    arg = exp.array(exp.cast(arg, exp.DType.TEXT))
4325
4326                arrays.append(arg)
4327
4328            array = (
4329                exp.ArrayConcat(this=arrays[0], expressions=arrays[1:])
4330                if len(arrays) > 1
4331                else arrays[0]
4332            )
4333            return self.func("CONCAT_WS", separator, self.func("ARRAY_TO_STRING", array, separator))
4334
4335        return super().concatws_sql(expression)
def regexpextract_sql(self, expression: sqlglot.expressions.string.RegexpExtract) -> str:
4409    def regexpextract_sql(self, expression: exp.RegexpExtract) -> str:
4410        return self._regexp_extract_sql(expression)
def regexpextractall_sql(self, expression: sqlglot.expressions.string.RegexpExtractAll) -> str:
4412    def regexpextractall_sql(self, expression: exp.RegexpExtractAll) -> str:
4413        return self._regexp_extract_sql(expression)
def regexpinstr_sql(self, expression: sqlglot.expressions.string.RegexpInstr) -> str:
4415    def regexpinstr_sql(self, expression: exp.RegexpInstr) -> str:
4416        this = expression.this
4417        pattern = expression.expression
4418        position = expression.args.get("position")
4419        orig_occ = expression.args.get("occurrence")
4420        occurrence = orig_occ or exp.Literal.number(1)
4421        option = expression.args.get("option")
4422        parameters = expression.args.get("parameters")
4423
4424        validated_flags = self._validate_regexp_flags(parameters, supported_flags="ims")
4425        # Checked before the flags are embedded, which would make the pattern non-empty
4426        is_empty = pattern.copy().eq(exp.Literal.string(""))
4427        if validated_flags:
4428            pattern = exp.Concat(expressions=[exp.Literal.string(f"(?{validated_flags})"), pattern])
4429
4430        # Handle starting position offset
4431        pos_offset: exp.Expr = exp.Literal.number(0)
4432        if position and (not position.is_int or position.to_py() > 1):
4433            this = exp.Substring(this=this, start=position)
4434            pos_offset = position - exp.Literal.number(1)
4435
4436        # Helper: LIST_SUM(LIST_TRANSFORM(list[1:end], x -> LENGTH(x)))
4437        def sum_lengths(func_name: str, end: exp.Expr) -> exp.Expr:
4438            lst = exp.Bracket(
4439                this=exp.Anonymous(this=func_name, expressions=[this, pattern]),
4440                expressions=[exp.Slice(this=exp.Literal.number(1), expression=end)],
4441                offset=1,
4442            )
4443            transform = exp.Anonymous(
4444                this="LIST_TRANSFORM",
4445                expressions=[
4446                    lst,
4447                    exp.Lambda(
4448                        this=exp.Length(this=exp.to_identifier("x")),
4449                        expressions=[exp.to_identifier("x")],
4450                    ),
4451                ],
4452            )
4453            return exp.Coalesce(
4454                this=exp.Anonymous(this="LIST_SUM", expressions=[transform]),
4455                expressions=[exp.Literal.number(0)],
4456            )
4457
4458        # Position = 1 + sum(split_lengths[1:occ]) + sum(match_lengths[1:occ-1]) + offset
4459        base_pos: exp.Expr = (
4460            exp.Literal.number(1)
4461            + sum_lengths("STRING_SPLIT_REGEX", occurrence)
4462            + sum_lengths("REGEXP_EXTRACT_ALL", occurrence - exp.Literal.number(1))
4463            + pos_offset
4464        )
4465
4466        # option=1: add match length for end position
4467        if option and option.is_int and option.to_py() == 1:
4468            match_at_occ = exp.Bracket(
4469                this=exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern]),
4470                expressions=[occurrence],
4471                offset=1,
4472            )
4473            base_pos = base_pos + exp.Coalesce(
4474                this=exp.Length(this=match_at_occ), expressions=[exp.Literal.number(0)]
4475            )
4476
4477        # NULL checks for all provided arguments
4478        # .copy() is used strictly because .is_() alters the node's parent pointer, mutating the parsed AST
4479        null_args = [
4480            expression.this,
4481            expression.expression,
4482            position,
4483            orig_occ,
4484            option,
4485            parameters,
4486        ]
4487        null_checks = [arg.copy().is_(exp.Null()) for arg in null_args if arg]
4488
4489        matches = exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern])
4490
4491        return self.sql(
4492            exp.case()
4493            .when(exp.or_(*null_checks), exp.Null())
4494            .when(is_empty, exp.Literal.number(0))
4495            .when(exp.Length(this=matches) < occurrence, exp.Literal.number(0))
4496            .else_(base_pos)
4497        )
@unsupported_args('culture')
def numbertostr_sql(self, expression: sqlglot.expressions.string.NumberToStr) -> str:
4499    @unsupported_args("culture")
4500    def numbertostr_sql(self, expression: exp.NumberToStr) -> str:
4501        fmt = expression.args.get("format")
4502        if fmt and fmt.is_int:
4503            return self.func("FORMAT", f"'{{:,.{fmt.name}f}}'", expression.this)
4504
4505        self.unsupported("Only integer formats are supported by NumberToStr")
4506        return self.function_fallback_sql(expression)
def autoincrementcolumnconstraint_sql(self, _) -> str:
4508    def autoincrementcolumnconstraint_sql(self, _) -> str:
4509        self.unsupported("The AUTOINCREMENT column constraint is not supported by DuckDB")
4510        return ""
def aliases_sql(self, expression: sqlglot.expressions.core.Aliases) -> str:
4512    def aliases_sql(self, expression: exp.Aliases) -> str:
4513        this = expression.this
4514        if isinstance(this, exp.Posexplode):
4515            return self.posexplode_sql(this)
4516
4517        return super().aliases_sql(expression)
def posexplode_sql(self, expression: sqlglot.expressions.array.Posexplode) -> str:
4519    def posexplode_sql(self, expression: exp.Posexplode) -> str:
4520        this = expression.this
4521        parent = expression.parent
4522
4523        # The default Spark aliases are "pos" and "col", unless specified otherwise
4524        pos, col = exp.to_identifier("pos"), exp.to_identifier("col")
4525
4526        if isinstance(parent, exp.Aliases):
4527            # Column case: SELECT POSEXPLODE(col) [AS (a, b)]
4528            pos, col = parent.expressions
4529        elif isinstance(parent, exp.Table):
4530            # Table case: SELECT * FROM POSEXPLODE(col) [AS (a, b)]
4531            alias = parent.args.get("alias")
4532            if alias:
4533                pos, col = alias.columns or [pos, col]
4534                alias.pop()
4535
4536        # Translate POSEXPLODE to UNNEST + GENERATE_SUBSCRIPTS
4537        # Note: In Spark pos is 0-indexed, but in DuckDB it's 1-indexed, so we subtract 1 from GENERATE_SUBSCRIPTS
4538        unnest_sql = self.sql(exp.Unnest(expressions=[this], alias=col))
4539        gen_subscripts = self.sql(
4540            exp.Alias(
4541                this=exp.Anonymous(
4542                    this="GENERATE_SUBSCRIPTS", expressions=[this, exp.Literal.number(1)]
4543                )
4544                - exp.Literal.number(1),
4545                alias=pos,
4546            )
4547        )
4548
4549        posexplode_sql = self.format_args(gen_subscripts, unnest_sql)
4550
4551        if isinstance(parent, exp.From) or (parent and isinstance(parent.parent, exp.From)):
4552            # SELECT * FROM POSEXPLODE(col) -> SELECT * FROM (SELECT GENERATE_SUBSCRIPTS(...), UNNEST(...))
4553            return self.sql(exp.Subquery(this=exp.Select(expressions=[posexplode_sql])))
4554
4555        return posexplode_sql
def addmonths_sql(self, expression: sqlglot.expressions.temporal.AddMonths) -> str:
4557    def addmonths_sql(self, expression: exp.AddMonths) -> str:
4558        """
4559        Handles three key issues:
4560        1. Float/decimal months: e.g., Snowflake rounds, whereas DuckDB INTERVAL requires integers
4561        2. End-of-month preservation: If input is last day of month, result is last day of result month
4562        3. Type preservation: Maintains DATE/TIMESTAMPTZ types (DuckDB defaults to TIMESTAMP)
4563        """
4564        from sqlglot.optimizer.annotate_types import annotate_types
4565
4566        this = expression.this
4567        if not this.type:
4568            this = annotate_types(this, dialect=self.dialect)
4569
4570        if this.is_type(*exp.DataType.TEXT_TYPES):
4571            this = exp.Cast(this=this, to=exp.DataType(this=exp.DType.TIMESTAMP))
4572
4573        # Detect float/decimal months to apply rounding (Snowflake behavior)
4574        # DuckDB INTERVAL syntax doesn't support non-integer expressions, so use TO_MONTHS
4575        months_expr = expression.expression
4576        if not months_expr.type:
4577            months_expr = annotate_types(months_expr, dialect=self.dialect)
4578
4579        # Build interval or to_months expression based on type
4580        # Float/decimal case: Round and use TO_MONTHS(CAST(ROUND(value) AS INT))
4581        interval_or_to_months = (
4582            exp.func("TO_MONTHS", exp.cast(exp.func("ROUND", months_expr), "INT"))
4583            if months_expr.is_type(
4584                exp.DType.FLOAT,
4585                exp.DType.DOUBLE,
4586                exp.DType.DECIMAL,
4587            )
4588            # Integer case: standard INTERVAL N MONTH syntax
4589            else exp.Interval(this=months_expr, unit=exp.var("MONTH"))
4590        )
4591
4592        date_add_expr = exp.Add(this=this, expression=interval_or_to_months)
4593
4594        # Apply end-of-month preservation if Snowflake flag is set
4595        # CASE WHEN LAST_DAY(date) = date THEN LAST_DAY(result) ELSE result END
4596        preserve_eom = expression.args.get("preserve_end_of_month")
4597        result_expr = (
4598            exp.case()
4599            .when(
4600                exp.EQ(this=exp.func("LAST_DAY", this), expression=this),
4601                exp.func("LAST_DAY", date_add_expr),
4602            )
4603            .else_(date_add_expr)
4604            if preserve_eom
4605            else date_add_expr
4606        )
4607
4608        # DuckDB's DATE_ADD function returns TIMESTAMP/DATETIME by default, even when the input is DATE
4609        # To match for example Snowflake's ADD_MONTHS behavior (which preserves the input type)
4610        # We need to cast the result back to the original type when the input is DATE or TIMESTAMPTZ
4611        # Example: ADD_MONTHS('2023-01-31'::date, 1) should return DATE, not TIMESTAMP
4612        if this.is_type(exp.DType.DATE, exp.DType.TIMESTAMPTZ):
4613            return self.sql(exp.Cast(this=result_expr, to=this.type))
4614        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:
4616    def format_sql(self, expression: exp.Format) -> str:
4617        if expression.name.lower() == "%s" and len(expression.expressions) == 1:
4618            return self.func("FORMAT", "'{}'", expression.expressions[0])
4619
4620        return self.function_fallback_sql(expression)
def hexstring_sql( self, expression: sqlglot.expressions.query.HexString, binary_function_repr: str | None = None) -> str:
4622    def hexstring_sql(
4623        self, expression: exp.HexString, binary_function_repr: str | None = None
4624    ) -> str:
4625        # UNHEX('FF') correctly produces blob \xFF in DuckDB
4626        return super().hexstring_sql(expression, binary_function_repr="UNHEX")
def datetrunc_sql(self, expression: sqlglot.expressions.temporal.DateTrunc) -> str:
4628    def datetrunc_sql(self, expression: exp.DateTrunc) -> str:
4629        unit = expression.args.get("unit")
4630        date = expression.this
4631
4632        week_start = _week_trunc_start_dow(unit)
4633        unit = unit_to_str(expression)
4634
4635        if week_start:
4636            result = self.sql(
4637                _build_week_trunc_expression(date, week_start, preserve_start_day=True)
4638            )
4639        else:
4640            result = self.func("DATE_TRUNC", unit, date)
4641
4642        if expression.args.get("input_type_preserved") and date.is_type(
4643            *exp.DataType.TEMPORAL_TYPES
4644        ):
4645            return self.sql(exp.Cast(this=result, to=date.type))
4646
4647        return result
def datetimetrunc_sql(self, expression: sqlglot.expressions.temporal.DatetimeTrunc) -> str:
4649    def datetimetrunc_sql(self, expression: exp.DatetimeTrunc) -> str:
4650        this = exp.cast(expression.this, exp.DType.DATETIME)
4651        week_start = _week_trunc_start_dow(expression.args.get("unit"))
4652        if week_start:
4653            return self.sql(
4654                _build_week_trunc_expression(
4655                    this, week_start, preserve_start_day=True, cast_to_date=False
4656                )
4657            )
4658
4659        return self.func("DATE_TRUNC", unit_to_str(expression), this)
def timestamptrunc_sql(self, expression: sqlglot.expressions.temporal.TimestampTrunc) -> str:
4661    def timestamptrunc_sql(self, expression: exp.TimestampTrunc) -> str:
4662        zone = expression.args.get("zone")
4663        timestamp = expression.this
4664        week_start = _week_trunc_start_dow(expression.args.get("unit"))
4665
4666        # The week start emulation below is exact, so avoid weekstart_unit_to_str's degrade warning
4667        unit = unit_to_str(expression) if week_start else weekstart_unit_to_str(self, expression)
4668        date_unit = is_date_unit(unit) or bool(week_start)
4669
4670        def _trunc_expr(this: exp.Expr) -> exp.Expr:
4671            if week_start:
4672                return _build_week_trunc_expression(
4673                    this, week_start, preserve_start_day=True, cast_to_date=False
4674                )
4675            return exp.func("DATE_TRUNC", unit, this)
4676
4677        if date_unit and zone:
4678            # BigQuery's TIMESTAMP_TRUNC with timezone truncates in the target timezone and returns as UTC.
4679            # Double AT TIME ZONE needed for BigQuery compatibility:
4680            # 1. First AT TIME ZONE: ensures truncation happens in the target timezone
4681            # 2. Second AT TIME ZONE: converts the DATE result back to TIMESTAMPTZ (preserving time component)
4682            timestamp = exp.AtTimeZone(this=timestamp, zone=zone)
4683            trunced = _trunc_expr(timestamp)
4684            if isinstance(trunced, exp.DateAdd):
4685                # Parenthesize so the trailing AT TIME ZONE binds to the whole shifted expression
4686                trunced = exp.Paren(this=trunced)
4687            return self.sql(exp.AtTimeZone(this=trunced, zone=zone))
4688
4689        result = self.sql(_trunc_expr(timestamp))
4690        if expression.args.get("input_type_preserved"):
4691            if timestamp.type and timestamp.is_type(exp.DType.TIME, exp.DType.TIMETZ):
4692                dummy_date = exp.Cast(
4693                    this=exp.Literal.string("1970-01-01"),
4694                    to=exp.DataType(this=exp.DType.DATE),
4695                )
4696                date_time = exp.Add(this=dummy_date, expression=timestamp)
4697                result = self.func("DATE_TRUNC", unit, date_time)
4698                return self.sql(exp.Cast(this=result, to=timestamp.type))
4699
4700            if timestamp.is_type(*exp.DataType.TEMPORAL_TYPES):
4701                return self.sql(exp.Cast(this=result, to=timestamp.type))
4702
4703        return result
def trim_sql(self, expression: sqlglot.expressions.string.Trim) -> str:
4705    def trim_sql(self, expression: exp.Trim) -> str:
4706        expression.this.replace(_cast_to_varchar(expression.this))
4707        if expression.expression:
4708            expression.expression.replace(_cast_to_varchar(expression.expression))
4709
4710        result_sql = super().trim_sql(expression)
4711        return _gen_with_cast_to_blob(self, expression, result_sql)
def round_sql(self, expression: sqlglot.expressions.math.Round) -> str:
4713    def round_sql(self, expression: exp.Round) -> str:
4714        this = expression.this
4715        decimals = expression.args.get("decimals")
4716        truncate = expression.args.get("truncate")
4717
4718        # DuckDB requires the scale (decimals) argument to be an INT
4719        # Some dialects (e.g., Snowflake) allow non-integer scales and cast to an integer internally
4720        if decimals is not None and expression.args.get("casts_non_integer_decimals"):
4721            if not (decimals.is_int or decimals.is_type(*exp.DataType.INTEGER_TYPES)):
4722                decimals = exp.cast(decimals, exp.DType.INT)
4723
4724        func = "ROUND"
4725        if truncate:
4726            # BigQuery uses ROUND_HALF_EVEN; Snowflake uses HALF_TO_EVEN
4727            if truncate.this in ("ROUND_HALF_EVEN", "HALF_TO_EVEN"):
4728                func = "ROUND_EVEN"
4729                truncate = None
4730            # BigQuery uses ROUND_HALF_AWAY_FROM_ZERO; Snowflake uses HALF_AWAY_FROM_ZERO
4731            elif truncate.this in ("ROUND_HALF_AWAY_FROM_ZERO", "HALF_AWAY_FROM_ZERO"):
4732                truncate = None
4733
4734        return self.func(func, this, decimals, truncate)
def trycast_sql(self, expression: sqlglot.expressions.functions.TryCast) -> str:
4736    def trycast_sql(self, expression: exp.TryCast) -> str:
4737        to = expression.to
4738        to_type = to.this
4739        src = expression.this
4740
4741        if (
4742            expression.args.get("null_on_text_overflow")
4743            and to_type in exp.DataType.TEXT_TYPES
4744            and to.expressions
4745        ):
4746            return self.sql(
4747                exp.case()
4748                .when(
4749                    exp.LTE(this=exp.func("LENGTH", src), expression=to.expressions[0].this),
4750                    exp.cast(src, "TEXT"),
4751                )
4752                .else_(exp.Null())
4753            )
4754        elif to_type == exp.DType.DATE and expression.args.get("probe_date_format"):
4755            slash_strptime = exp.cast(
4756                exp.func("TRY_STRPTIME", src, exp.Literal.string(self._TRYCAST_DATE_SLASH_FMT)),
4757                "DATE",
4758            )
4759            mon_strptime = exp.cast(
4760                exp.func("TRY_STRPTIME", src, exp.Literal.string(self._TRYCAST_DATE_MON_FMT)),
4761                "DATE",
4762            )
4763            return self.sql(
4764                exp.case()
4765                .when(exp.func("CONTAINS", src, exp.Literal.string("/")), slash_strptime)
4766                .when(
4767                    exp.RegexpLike(this=src, expression=exp.Literal.string("[A-Za-z]")),
4768                    mon_strptime,
4769                )
4770                .else_(exp.TryCast(this=src, to=to))
4771            )
4772        elif (
4773            isinstance(to_type, exp.Interval)
4774            and (unit := to_type.unit)
4775            and expression.args.get("requires_string")
4776        ):
4777            interval_type = exp.DataType.build("INTERVAL")
4778            if isinstance(unit, exp.IntervalSpan):
4779                self.unsupported(
4780                    "TRY_CAST to INTERVAL with span (e.g. HOUR TO MINUTE) is not supported in DuckDB"
4781                )
4782                return self.sql(exp.TryCast(this=src, to=interval_type))
4783            return self.sql(
4784                exp.TryCast(
4785                    this=exp.DPipe(this=src, expression=exp.Literal.string(f" {unit.name}")),
4786                    to=interval_type,
4787                )
4788            )
4789
4790        return super().trycast_sql(expression)
def strtok_sql(self, expression: sqlglot.expressions.string.Strtok) -> str:
4792    def strtok_sql(self, expression: exp.Strtok) -> str:
4793        string_arg = expression.this
4794        delimiter_arg = expression.args.get("delimiter")
4795        part_index_arg = expression.args.get("part_index")
4796
4797        if delimiter_arg and part_index_arg:
4798            # Escape regex chars and build character class at runtime using REGEXP_REPLACE
4799            escaped_delimiter = exp.Anonymous(
4800                this="REGEXP_REPLACE",
4801                expressions=[
4802                    delimiter_arg,
4803                    exp.Literal.string(
4804                        r"([\[\]^.\-*+?(){}|$\\])"
4805                    ),  # Escape problematic regex chars
4806                    exp.Literal.string(
4807                        r"\\\1"
4808                    ),  # Replace with escaped version using $1 backreference
4809                    exp.Literal.string("g"),  # Global flag
4810                ],
4811            )
4812            # CASE WHEN delimiter = '' THEN '' ELSE CONCAT('[', escaped_delimiter, ']') END
4813            regex_pattern = (
4814                exp.case()
4815                .when(delimiter_arg.eq(exp.Literal.string("")), exp.Literal.string(""))
4816                .else_(
4817                    exp.func(
4818                        "CONCAT",
4819                        exp.Literal.string("["),
4820                        escaped_delimiter,
4821                        exp.Literal.string("]"),
4822                    )
4823                )
4824            )
4825
4826            # STRTOK skips empty strings, so we need to filter them out
4827            # LIST_FILTER(REGEXP_SPLIT_TO_ARRAY(string, pattern), x -> x != '')[index]
4828            split_array = exp.func("REGEXP_SPLIT_TO_ARRAY", string_arg, regex_pattern)
4829            x = exp.to_identifier("x")
4830            is_empty = x.eq(exp.Literal.string(""))
4831            filtered_array = exp.func(
4832                "LIST_FILTER",
4833                split_array,
4834                exp.Lambda(this=exp.not_(is_empty.copy()), expressions=[x.copy()]),
4835            )
4836            base_func = exp.Bracket(
4837                this=filtered_array,
4838                expressions=[part_index_arg],
4839                offset=1,
4840            )
4841
4842            # Use template with the built regex pattern
4843            result = exp.replace_placeholders(
4844                self.STRTOK_TEMPLATE.copy(),
4845                string=string_arg,
4846                delimiter=delimiter_arg,
4847                part_index=part_index_arg,
4848                base_func=base_func,
4849            )
4850
4851            return self.sql(result)
4852
4853        return self.function_fallback_sql(expression)
def strtoktoarray_sql(self, expression: sqlglot.expressions.array.StrtokToArray) -> str:
4855    def strtoktoarray_sql(self, expression: exp.StrtokToArray) -> str:
4856        string_arg = expression.this
4857        delimiter_arg = expression.args.get("expression") or exp.Literal.string(" ")
4858
4859        escaped = exp.RegexpReplace(
4860            this=delimiter_arg.copy(),
4861            expression=exp.Literal.string(r"([\[\]^.\-*+?(){}|$\\])"),
4862            replacement=exp.Literal.string(r"\\\1"),
4863            modifiers=exp.Literal.string("g"),
4864        )
4865        return self.sql(
4866            exp.replace_placeholders(
4867                self.STRTOK_TO_ARRAY_TEMPLATE.copy(),
4868                string=string_arg,
4869                delimiter=delimiter_arg,
4870                escaped=escaped,
4871            )
4872        )
def approxquantile_sql(self, expression: sqlglot.expressions.aggregate.ApproxQuantile) -> str:
4874    def approxquantile_sql(self, expression: exp.ApproxQuantile) -> str:
4875        result = self.func("APPROX_QUANTILE", expression.this, expression.args.get("quantile"))
4876
4877        # DuckDB returns integers for APPROX_QUANTILE, cast to DOUBLE if the expected type is a real type
4878        if expression.is_type(*exp.DataType.REAL_TYPES):
4879            result = f"CAST({result} AS DOUBLE)"
4880
4881        return result
def approxquantiles_sql(self, expression: sqlglot.expressions.aggregate.ApproxQuantiles) -> str:
4883    def approxquantiles_sql(self, expression: exp.ApproxQuantiles) -> str:
4884        """
4885        BigQuery's APPROX_QUANTILES(expr, n) returns an array of n+1 approximate quantile values
4886        dividing the input distribution into n equal-sized buckets.
4887
4888        Both BigQuery and DuckDB use approximate algorithms for quantile estimation, but BigQuery
4889        does not document the specific algorithm used so results may differ. DuckDB does not
4890        support RESPECT NULLS.
4891        """
4892        this = expression.this
4893        if isinstance(this, exp.Distinct):
4894            # APPROX_QUANTILES requires 2 args and DISTINCT node grabs both
4895            if len(this.expressions) < 2:
4896                self.unsupported("APPROX_QUANTILES requires a bucket count argument")
4897                return self.function_fallback_sql(expression)
4898            num_quantiles_expr = this.expressions[1].pop()
4899        else:
4900            num_quantiles_expr = expression.expression
4901
4902        if not isinstance(num_quantiles_expr, exp.Literal) or not num_quantiles_expr.is_int:
4903            self.unsupported("APPROX_QUANTILES bucket count must be a positive integer")
4904            return self.function_fallback_sql(expression)
4905
4906        num_quantiles = t.cast(int, num_quantiles_expr.to_py())
4907        if num_quantiles <= 0:
4908            self.unsupported("APPROX_QUANTILES bucket count must be a positive integer")
4909            return self.function_fallback_sql(expression)
4910
4911        quantiles = [
4912            exp.Literal.number(Decimal(i) / Decimal(num_quantiles))
4913            for i in range(num_quantiles + 1)
4914        ]
4915
4916        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:
4918    def jsonextractscalar_sql(self, expression: exp.JSONExtractScalar) -> str:
4919        if expression.args.get("scalar_only"):
4920            json_value = exp.JSONExtractScalar(
4921                this=rename_func("JSON_VALUE")(self, expression), expression="'$'"
4922            )
4923
4924            # `->>` binds looser than most operators, so the wrap logic needs the parent
4925            json_value.parent = expression.parent
4926            expression = json_value
4927
4928        return _arrow_json_extract_sql(self, expression)
def bitwisenot_sql(self, expression: sqlglot.expressions.core.BitwiseNot) -> str:
4930    def bitwisenot_sql(self, expression: exp.BitwiseNot) -> str:
4931        this = expression.this
4932
4933        if _is_binary(this):
4934            expression.type = exp.DType.BINARY.into_expr()
4935
4936        arg = _cast_to_bit(this)
4937
4938        if isinstance(this, exp.Neg):
4939            arg = exp.Paren(this=arg)
4940
4941        expression.set("this", arg)
4942
4943        result_sql = f"~{self.sql(expression, 'this')}"
4944
4945        return _gen_with_cast_to_blob(self, expression, result_sql)
def window_sql(self, expression: sqlglot.expressions.query.Window) -> str:
4947    def window_sql(self, expression: exp.Window) -> str:
4948        this = expression.this
4949        if isinstance(this, exp.Corr) or (
4950            isinstance(this, exp.Filter) and isinstance(this.this, exp.Corr)
4951        ):
4952            return self._corr_sql(expression)
4953
4954        return super().window_sql(expression)
def filter_sql(self, expression: sqlglot.expressions.core.Filter) -> str:
4956    def filter_sql(self, expression: exp.Filter) -> str:
4957        if isinstance(expression.this, exp.Corr):
4958            return self._corr_sql(expression)
4959
4960        return super().filter_sql(expression)
def uuid_sql(self, expression: sqlglot.expressions.functions.Uuid) -> str:
4979    def uuid_sql(self, expression: exp.Uuid) -> str:
4980        namespace = expression.this
4981        name = expression.args.get("name")
4982
4983        # UUID v5 (namespace + name) - Emulate using SHA1
4984        if namespace and name:
4985            result = exp.replace_placeholders(
4986                self.UUID_V5_TEMPLATE.copy(),
4987                namespace=namespace,
4988                name=name,
4989            )
4990            return self.sql(result)
4991
4992        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
MERGE_ALL_BY_NAME
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
ALTER_TABLE_ADD_COLUMNS_WRAPPED
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
BYTE_STRING_FUNCTION
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
ALTER_PARTITION_OPTIONS
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
rawstring_sql
datatypeparam_sql
datatype_param_bound_limiter
directory_sql
delete_sql
drop_sql
set_operation
set_operations
fetch_sql
limitoptions_sql
per_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
lockingstatement_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
connectto_sql
disconnect_sql
connect_sql
prior_sql
nearest_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
select_sql
schema_sql
schema_columns_sql
star_sql
parameter_sql
sessionparameter_sql
subquery_sql
qualify_sql
prewhere_sql
where_sql
partition_by_sql
partitionbyexcluding_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
teradatatitle_sql
jsonarray_sql
jsonarrayagg_sql
jsoncolumndef_sql
jsonschema_sql
jsontable_sql
openjsoncolumndef_sql
openjson_sql
in_unnest_op
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
replacecolumns_sql
addpartition_sql
distinct_sql
havingmax_sql
intdiv_sql
dpipe_sql
nullif_zero_divisor
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
function_name
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
aggregatekeyproperty_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
min_sql
max_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
arraysort_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
liststage_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
weightstring_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