Edit on GitHub

sqlglot.generators.duckdb

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

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

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

Transform Snowflake's TIME_SLICE to DuckDB's time_bucket.

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

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

def bitmapbucketnumber_sql(self, expression: sqlglot.expressions.math.BitmapBucketNumber) -> str:
2357    def bitmapbucketnumber_sql(self, expression: exp.BitmapBucketNumber) -> str:
2358        """
2359        Transpile BITMAP_BUCKET_NUMBER function from Snowflake to DuckDB equivalent.
2360
2361        Snowflake's BITMAP_BUCKET_NUMBER returns a 1-based bucket identifier where:
2362        - Each bucket covers 32,768 values
2363        - Bucket numbering starts at 1
2364        - Formula: ((value - 1) // 32768) + 1 for positive values
2365
2366        For non-positive values (0 and negative), we use value // 32768 to avoid
2367        producing bucket 0 or positive bucket IDs for negative inputs.
2368        """
2369        value = expression.this
2370
2371        positive_formula = ((value - 1) // 32768) + 1
2372        non_positive_formula = value // 32768
2373
2374        # CASE WHEN value > 0 THEN ((value - 1) // 32768) + 1 ELSE value // 32768 END
2375        case_expr = (
2376            exp.case()
2377            .when(exp.GT(this=value, expression=exp.Literal.number(0)), positive_formula)
2378            .else_(non_positive_formula)
2379        )
2380        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:
2382    def bitmapbitposition_sql(self, expression: exp.BitmapBitPosition) -> str:
2383        """
2384        Transpile Snowflake's BITMAP_BIT_POSITION to DuckDB CASE expression.
2385
2386        Snowflake's BITMAP_BIT_POSITION behavior:
2387        - For n <= 0: returns ABS(n) % 32768
2388        - For n > 0: returns (n - 1) % 32768 (maximum return value is 32767)
2389        """
2390        this = expression.this
2391
2392        return self.sql(
2393            exp.Mod(
2394                this=exp.Paren(
2395                    this=exp.If(
2396                        this=exp.GT(this=this, expression=exp.Literal.number(0)),
2397                        true=this - exp.Literal.number(1),
2398                        false=exp.Abs(this=this),
2399                    )
2400                ),
2401                expression=MAX_BIT_POSITION,
2402            )
2403        )

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

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:
2419    def getignorecase_sql(self, expression: exp.GetIgnoreCase) -> str:
2420        self.unsupported("DuckDB does not support the GET_IGNORE_CASE() function")
2421        return self.function_fallback_sql(expression)
def compress_sql(self, expression: sqlglot.expressions.string.Compress) -> str:
2423    def compress_sql(self, expression: exp.Compress) -> str:
2424        self.unsupported("DuckDB does not support the COMPRESS() function")
2425        return self.function_fallback_sql(expression)
def encrypt_sql(self, expression: sqlglot.expressions.string.Encrypt) -> str:
2427    def encrypt_sql(self, expression: exp.Encrypt) -> str:
2428        self.unsupported("ENCRYPT is not supported in DuckDB")
2429        return self.function_fallback_sql(expression)
def decrypt_sql(self, expression: sqlglot.expressions.string.Decrypt) -> str:
2431    def decrypt_sql(self, expression: exp.Decrypt) -> str:
2432        func_name = "TRY_DECRYPT" if expression.args.get("safe") else "DECRYPT"
2433        self.unsupported(f"{func_name} is not supported in DuckDB")
2434        return self.function_fallback_sql(expression)
def decryptraw_sql(self, expression: sqlglot.expressions.string.DecryptRaw) -> str:
2436    def decryptraw_sql(self, expression: exp.DecryptRaw) -> str:
2437        func_name = "TRY_DECRYPT_RAW" if expression.args.get("safe") else "DECRYPT_RAW"
2438        self.unsupported(f"{func_name} is not supported in DuckDB")
2439        return self.function_fallback_sql(expression)
def encryptraw_sql(self, expression: sqlglot.expressions.string.EncryptRaw) -> str:
2441    def encryptraw_sql(self, expression: exp.EncryptRaw) -> str:
2442        self.unsupported("ENCRYPT_RAW is not supported in DuckDB")
2443        return self.function_fallback_sql(expression)
def parseurl_sql(self, expression: sqlglot.expressions.string.ParseUrl) -> str:
2445    def parseurl_sql(self, expression: exp.ParseUrl) -> str:
2446        self.unsupported("PARSE_URL is not supported in DuckDB")
2447        return self.function_fallback_sql(expression)
def parseip_sql(self, expression: sqlglot.expressions.functions.ParseIp) -> str:
2449    def parseip_sql(self, expression: exp.ParseIp) -> str:
2450        self.unsupported("PARSE_IP is not supported in DuckDB")
2451        return self.function_fallback_sql(expression)
def decompressstring_sql(self, expression: sqlglot.expressions.string.DecompressString) -> str:
2453    def decompressstring_sql(self, expression: exp.DecompressString) -> str:
2454        self.unsupported("DECOMPRESS_STRING is not supported in DuckDB")
2455        return self.function_fallback_sql(expression)
def decompressbinary_sql(self, expression: sqlglot.expressions.string.DecompressBinary) -> str:
2457    def decompressbinary_sql(self, expression: exp.DecompressBinary) -> str:
2458        self.unsupported("DECOMPRESS_BINARY is not supported in DuckDB")
2459        return self.function_fallback_sql(expression)
def jarowinklersimilarity_sql(self, expression: sqlglot.expressions.math.JarowinklerSimilarity) -> str:
2461    def jarowinklersimilarity_sql(self, expression: exp.JarowinklerSimilarity) -> str:
2462        this = expression.this
2463        expr = expression.expression
2464
2465        if expression.args.get("case_insensitive"):
2466            this = exp.Upper(this=this)
2467            expr = exp.Upper(this=expr)
2468
2469        result = exp.func("JARO_WINKLER_SIMILARITY", this, expr)
2470
2471        if expression.args.get("integer_scale"):
2472            result = exp.cast(result * 100, "INTEGER")
2473
2474        return self.sql(result)
def nthvalue_sql(self, expression: sqlglot.expressions.aggregate.NthValue) -> str:
2476    def nthvalue_sql(self, expression: exp.NthValue) -> str:
2477        from_first = expression.args.get("from_first", True)
2478        if not from_first:
2479            self.unsupported("DuckDB's NTH_VALUE doesn't support starting from the end ")
2480
2481        return self.function_fallback_sql(expression)
def randstr_sql(self, expression: sqlglot.expressions.functions.Randstr) -> str:
2483    def randstr_sql(self, expression: exp.Randstr) -> str:
2484        """
2485        Transpile Snowflake's RANDSTR to DuckDB equivalent using deterministic hash-based random.
2486        Uses a pre-parsed template with placeholders replaced by expression nodes.
2487
2488        RANDSTR(length, generator) generates a random string of specified length.
2489        - With numeric seed: Use HASH(i + seed) for deterministic output (same seed = same result)
2490        - With RANDOM(): Use RANDOM() in the hash for non-deterministic output
2491        - No generator: Use default seed value
2492        """
2493        length = expression.this
2494        generator = expression.args.get("generator")
2495
2496        if generator:
2497            if isinstance(generator, exp.Rand):
2498                # If it's RANDOM(), use its seed if available, otherwise use RANDOM() itself
2499                seed_value = generator.this or generator
2500            else:
2501                # Const/int or other expression - use as seed directly
2502                seed_value = generator
2503        else:
2504            # No generator specified, use default seed (arbitrary but deterministic)
2505            seed_value = exp.Literal.number(RANDSTR_SEED)
2506
2507        replacements = {"seed": seed_value, "length": length}
2508        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:
2510    @unsupported_args("finish")
2511    def reduce_sql(self, expression: exp.Reduce) -> str:
2512        array_arg = expression.this
2513        initial_value = expression.args.get("initial")
2514        merge_lambda = expression.args.get("merge")
2515
2516        if merge_lambda:
2517            merge_lambda.set("colon", True)
2518
2519        return self.func("list_reduce", array_arg, merge_lambda, initial_value)
def zipf_sql(self, expression: sqlglot.expressions.functions.Zipf) -> str:
2521    def zipf_sql(self, expression: exp.Zipf) -> str:
2522        """
2523        Transpile Snowflake's ZIPF to DuckDB using CDF-based inverse sampling.
2524        Uses a pre-parsed template with placeholders replaced by expression nodes.
2525        """
2526        s = expression.this
2527        n = expression.args["elementcount"]
2528        gen = expression.args["gen"]
2529
2530        if not isinstance(gen, exp.Rand):
2531            # (ABS(HASH(seed)) % 1000000) / 1000000.0
2532            random_expr: exp.Expr = exp.Div(
2533                this=exp.Paren(
2534                    this=exp.Mod(
2535                        this=exp.Abs(this=exp.Anonymous(this="HASH", expressions=[gen.copy()])),
2536                        expression=exp.Literal.number(1000000),
2537                    )
2538                ),
2539                expression=exp.Literal.number(1000000.0),
2540            )
2541        else:
2542            # Use RANDOM() for non-deterministic output
2543            random_expr = exp.Rand()
2544
2545        replacements = {"s": s, "n": n, "random_expr": random_expr}
2546        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:
2548    def tobinary_sql(self, expression: exp.ToBinary) -> str:
2549        """
2550        TO_BINARY and TRY_TO_BINARY transpilation:
2551        - 'HEX': TO_BINARY('48454C50', 'HEX') -> UNHEX('48454C50')
2552        - 'UTF-8': TO_BINARY('TEST', 'UTF-8') -> ENCODE('TEST')
2553        - 'BASE64': TO_BINARY('SEVMUA==', 'BASE64') -> FROM_BASE64('SEVMUA==')
2554
2555        For TRY_TO_BINARY (safe=True), wrap with TRY():
2556        - 'HEX': TRY_TO_BINARY('invalid', 'HEX') -> TRY(UNHEX('invalid'))
2557        """
2558        value = expression.this
2559        format_arg = expression.args.get("format")
2560        is_safe = expression.args.get("safe")
2561        is_binary = _is_binary(expression)
2562
2563        if not format_arg and not is_binary:
2564            func_name = "TRY_TO_BINARY" if is_safe else "TO_BINARY"
2565            return self.func(func_name, value)
2566
2567        # Snowflake defaults to HEX encoding when no format is specified
2568        fmt = format_arg.name.upper() if format_arg else "HEX"
2569
2570        if fmt in ("UTF-8", "UTF8"):
2571            # DuckDB ENCODE always uses UTF-8, no charset parameter needed
2572            result = self.func("ENCODE", value)
2573        elif fmt == "BASE64":
2574            result = self.func("FROM_BASE64", value)
2575        elif fmt == "HEX":
2576            result = self.func("UNHEX", value)
2577        else:
2578            if is_safe:
2579                return self.sql(exp.null())
2580            else:
2581                self.unsupported(f"format {fmt} is not supported")
2582                result = self.func("TO_BINARY", value)
2583        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:
2585    def tonumber_sql(self, expression: exp.ToNumber) -> str:
2586        fmt = expression.args.get("format")
2587        precision = expression.args.get("precision")
2588        scale = expression.args.get("scale")
2589
2590        if not fmt and precision and scale:
2591            return self.sql(
2592                exp.cast(
2593                    expression.this, f"DECIMAL({precision.name}, {scale.name})", dialect="duckdb"
2594                )
2595            )
2596
2597        return super().tonumber_sql(expression)
def generator_sql(self, expression: sqlglot.expressions.array.Generator) -> str:
2623    def generator_sql(self, expression: exp.Generator) -> str:
2624        # Transpile Snowflake GENERATOR to DuckDB range()
2625        rowcount = expression.args.get("rowcount")
2626        time_limit = expression.args.get("time_limit")
2627
2628        if time_limit:
2629            self.unsupported("GENERATOR TIMELIMIT parameter is not supported in DuckDB")
2630
2631        if not rowcount:
2632            self.unsupported("GENERATOR without ROWCOUNT is not supported in DuckDB")
2633            return self.func("range", exp.Literal.number(0))
2634
2635        return self.func("range", rowcount)
def greatest_sql(self, expression: sqlglot.expressions.functions.Greatest) -> str:
2637    def greatest_sql(self, expression: exp.Greatest) -> str:
2638        return self._greatest_least_sql(expression)
def least_sql(self, expression: sqlglot.expressions.functions.Least) -> str:
2640    def least_sql(self, expression: exp.Least) -> str:
2641        return self._greatest_least_sql(expression)
def lambda_sql( self, expression: sqlglot.expressions.query.Lambda, arrow_sep: str = '->', wrap: bool = True) -> str:
2643    def lambda_sql(self, expression: exp.Lambda, arrow_sep: str = "->", wrap: bool = True) -> str:
2644        if expression.args.get("colon"):
2645            prefix = "LAMBDA "
2646            arrow_sep = ":"
2647            wrap = False
2648        else:
2649            prefix = ""
2650
2651        lambda_sql = super().lambda_sql(expression, arrow_sep=arrow_sep, wrap=wrap)
2652        return f"{prefix}{lambda_sql}"
def show_sql(self, expression: sqlglot.expressions.ddl.Show) -> str:
2654    def show_sql(self, expression: exp.Show) -> str:
2655        from_ = self.sql(expression, "from_")
2656        from_ = f" FROM {from_}" if from_ else ""
2657        return f"SHOW {expression.name}{from_}"
def soundex_sql(self, expression: sqlglot.expressions.string.Soundex) -> str:
2659    def soundex_sql(self, expression: exp.Soundex) -> str:
2660        self.unsupported("SOUNDEX is not supported in DuckDB")
2661        return self.func("SOUNDEX", expression.this)
def sortarray_sql(self, expression: sqlglot.expressions.array.SortArray) -> str:
2663    def sortarray_sql(self, expression: exp.SortArray) -> str:
2664        arr = expression.this
2665        asc = expression.args.get("asc")
2666        nulls_first = expression.args.get("nulls_first")
2667
2668        if not isinstance(asc, exp.Boolean) and not isinstance(nulls_first, exp.Boolean):
2669            return self.func("LIST_SORT", arr, asc, nulls_first)
2670
2671        nulls_are_first = nulls_first == exp.true()
2672        nulls_first_sql = exp.Literal.string("NULLS FIRST") if nulls_are_first else None
2673
2674        if not isinstance(asc, exp.Boolean):
2675            return self.func("LIST_SORT", arr, asc, nulls_first_sql)
2676
2677        descending = asc == exp.false()
2678
2679        if not descending and not nulls_are_first:
2680            return self.func("LIST_SORT", arr)
2681        if not nulls_are_first:
2682            return self.func("ARRAY_REVERSE_SORT", arr)
2683        return self.func(
2684            "LIST_SORT",
2685            arr,
2686            exp.Literal.string("DESC" if descending else "ASC"),
2687            exp.Literal.string("NULLS FIRST"),
2688        )
def install_sql(self, expression: sqlglot.expressions.ddl.Install) -> str:
2690    def install_sql(self, expression: exp.Install) -> str:
2691        force = "FORCE " if expression.args.get("force") else ""
2692        this = self.sql(expression, "this")
2693        from_clause = expression.args.get("from_")
2694        from_clause = f" FROM {from_clause}" if from_clause else ""
2695        return f"{force}INSTALL {this}{from_clause}"
def approxtopk_sql(self, expression: sqlglot.expressions.aggregate.ApproxTopK) -> str:
2697    def approxtopk_sql(self, expression: exp.ApproxTopK) -> str:
2698        self.unsupported(
2699            "APPROX_TOP_K cannot be transpiled to DuckDB due to incompatible return types. "
2700        )
2701        return self.function_fallback_sql(expression)
def strposition_sql(self, expression: sqlglot.expressions.string.StrPosition) -> str:
2703    def strposition_sql(self, expression: exp.StrPosition) -> str:
2704        this = expression.this
2705        substr = expression.args.get("substr")
2706        position = expression.args.get("position")
2707
2708        # For BINARY/BLOB: DuckDB's STRPOS doesn't support BLOB types
2709        # Convert to HEX strings, use STRPOS, then convert hex position to byte position
2710        if _is_binary(this):
2711            # Build expression: STRPOS(HEX(haystack), HEX(needle))
2712            hex_strpos = exp.StrPosition(
2713                this=exp.Hex(this=this),
2714                substr=exp.Hex(this=substr),
2715            )
2716
2717            return self.sql(exp.cast((hex_strpos + 1) / 2, exp.DType.INT))
2718
2719        # For VARCHAR: handle clamp_position
2720        if expression.args.get("clamp_position") and position:
2721            expression = expression.copy()
2722            expression.set(
2723                "position",
2724                exp.If(
2725                    this=exp.LTE(this=position, expression=exp.Literal.number(0)),
2726                    true=exp.Literal.number(1),
2727                    false=position.copy(),
2728                ),
2729            )
2730
2731        return strposition_sql(self, expression)
def substring_sql(self, expression: sqlglot.expressions.string.Substring) -> str:
2733    def substring_sql(self, expression: exp.Substring) -> str:
2734        if expression.args.get("zero_start"):
2735            start = expression.args.get("start")
2736            length = expression.args.get("length")
2737
2738            if start := expression.args.get("start"):
2739                start = exp.If(this=start.eq(0), true=exp.Literal.number(1), false=start)
2740            if length := expression.args.get("length"):
2741                length = exp.If(this=length < 0, true=exp.Literal.number(0), false=length)
2742
2743            return self.func("SUBSTRING", expression.this, start, length)
2744
2745        return self.function_fallback_sql(expression)
def strtotime_sql(self, expression: sqlglot.expressions.temporal.StrToTime) -> str:
2747    def strtotime_sql(self, expression: exp.StrToTime) -> str:
2748        # Check if target_type requires TIMESTAMPTZ (for LTZ/TZ variants)
2749        target_type = expression.args.get("target_type")
2750        needs_tz = target_type and target_type.this in (
2751            exp.DType.TIMESTAMPLTZ,
2752            exp.DType.TIMESTAMPTZ,
2753        )
2754
2755        if expression.args.get("safe"):
2756            formatted_time = self.format_time(expression)
2757            cast_type = exp.DType.TIMESTAMPTZ if needs_tz else exp.DType.TIMESTAMP
2758            return self.sql(
2759                exp.cast(self.func("TRY_STRPTIME", expression.this, formatted_time), cast_type)
2760            )
2761
2762        base_sql = str_to_time_sql(self, expression)
2763        if needs_tz:
2764            return self.sql(
2765                exp.cast(
2766                    base_sql,
2767                    exp.DataType(this=exp.DType.TIMESTAMPTZ),
2768                )
2769            )
2770        return base_sql
def strtodate_sql(self, expression: sqlglot.expressions.temporal.StrToDate) -> str:
2772    def strtodate_sql(self, expression: exp.StrToDate) -> str:
2773        formatted_time = self.format_time(expression)
2774        function_name = "STRPTIME" if not expression.args.get("safe") else "TRY_STRPTIME"
2775        return self.sql(
2776            exp.cast(
2777                self.func(function_name, expression.this, formatted_time),
2778                exp.DataType(this=exp.DType.DATE),
2779            )
2780        )
def parsedatetime_sql(self, expression: sqlglot.expressions.temporal.ParseDatetime) -> str:
2782    def parsedatetime_sql(self, expression: exp.ParseDatetime) -> str:
2783        formatted_time = self.format_time(expression)
2784
2785        default_year = expression.args.get("default_year")
2786        if default_year:
2787            year_str = exp.Literal.string(f"{default_year.name} ")
2788            fmt_prefix = exp.Literal.string("%Y ")
2789            value = exp.DPipe(this=year_str, expression=expression.this)
2790            fmt = exp.DPipe(this=fmt_prefix, expression=formatted_time)
2791            return self.func("STRPTIME", value, fmt)
2792
2793        return self.func("STRPTIME", expression.this, formatted_time)
def parsetime_sql(self, expression: sqlglot.expressions.temporal.ParseTime) -> str:
2795    def parsetime_sql(self, expression: exp.ParseTime) -> str:
2796        formatted_time = self.format_time(expression)
2797        return self.sql(
2798            exp.cast(
2799                self.func("STRPTIME", expression.this, formatted_time),
2800                exp.DataType(this=exp.DType.TIME),
2801            )
2802        )
def tsordstotime_sql(self, expression: sqlglot.expressions.temporal.TsOrDsToTime) -> str:
2804    def tsordstotime_sql(self, expression: exp.TsOrDsToTime) -> str:
2805        this = expression.this
2806        time_format = self.format_time(expression)
2807        safe = expression.args.get("safe")
2808        time_type = exp.DataType.from_str("TIME", dialect="duckdb")
2809        cast_expr = exp.TryCast if safe else exp.Cast
2810
2811        if time_format:
2812            func_name = "TRY_STRPTIME" if safe else "STRPTIME"
2813            strptime = exp.Anonymous(this=func_name, expressions=[this, time_format])
2814            return self.sql(cast_expr(this=strptime, to=time_type))
2815
2816        if isinstance(this, exp.TsOrDsToTime) or this.is_type(exp.DType.TIME):
2817            return self.sql(this)
2818
2819        return self.sql(cast_expr(this=this, to=time_type))
def currentdate_sql(self, expression: sqlglot.expressions.temporal.CurrentDate) -> str:
2821    def currentdate_sql(self, expression: exp.CurrentDate) -> str:
2822        if not expression.this:
2823            return "CURRENT_DATE"
2824
2825        expr = exp.Cast(
2826            this=exp.AtTimeZone(this=exp.CurrentTimestamp(), zone=expression.this),
2827            to=exp.DataType(this=exp.DType.DATE),
2828        )
2829        return self.sql(expr)
def checkjson_sql(self, expression: sqlglot.expressions.json.CheckJson) -> str:
2831    def checkjson_sql(self, expression: exp.CheckJson) -> str:
2832        arg = expression.this
2833        return self.sql(
2834            exp.case()
2835            .when(
2836                exp.or_(arg.is_(exp.Null()), arg.eq(""), exp.func("json_valid", arg)),
2837                exp.null(),
2838            )
2839            .else_(exp.Literal.string("Invalid JSON"))
2840        )
def parsejson_sql(self, expression: sqlglot.expressions.json.ParseJSON) -> str:
2842    def parsejson_sql(self, expression: exp.ParseJSON) -> str:
2843        arg = expression.this
2844        if expression.args.get("safe"):
2845            return self.sql(
2846                exp.case()
2847                .when(exp.func("json_valid", arg), exp.cast(arg.copy(), "JSON"))
2848                .else_(exp.null())
2849            )
2850        return self.func("JSON", arg)
def unicode_sql(self, expression: sqlglot.expressions.string.Unicode) -> str:
2852    def unicode_sql(self, expression: exp.Unicode) -> str:
2853        if expression.args.get("empty_is_zero"):
2854            return self.sql(
2855                exp.case()
2856                .when(expression.this.eq(exp.Literal.string("")), exp.Literal.number(0))
2857                .else_(exp.Anonymous(this="UNICODE", expressions=[expression.this]))
2858            )
2859
2860        return self.func("UNICODE", expression.this)
def stripnullvalue_sql(self, expression: sqlglot.expressions.json.StripNullValue) -> str:
2862    def stripnullvalue_sql(self, expression: exp.StripNullValue) -> str:
2863        return self.sql(
2864            exp.case()
2865            .when(exp.func("json_type", expression.this).eq("NULL"), exp.null())
2866            .else_(expression.this)
2867        )
def trunc_sql(self, expression: sqlglot.expressions.math.Trunc) -> str:
2869    def trunc_sql(self, expression: exp.Trunc) -> str:
2870        decimals = expression.args.get("decimals")
2871        if (
2872            expression.args.get("fractions_supported")
2873            and decimals
2874            and not decimals.is_type(exp.DType.INT)
2875        ):
2876            decimals = exp.cast(decimals, exp.DType.INT, dialect="duckdb")
2877
2878        return self.func("TRUNC", expression.this, decimals)
def normal_sql(self, expression: sqlglot.expressions.functions.Normal) -> str:
2880    def normal_sql(self, expression: exp.Normal) -> str:
2881        """
2882        Transpile Snowflake's NORMAL(mean, stddev, gen) to DuckDB.
2883
2884        Uses the Box-Muller transform via NORMAL_TEMPLATE.
2885        """
2886        mean = expression.this
2887        stddev = expression.args["stddev"]
2888        gen: exp.Expr = expression.args["gen"]
2889
2890        # Build two uniform random values [0, 1) for Box-Muller transform
2891        if isinstance(gen, exp.Rand) and gen.this is None:
2892            u1: exp.Expr = exp.Rand()
2893            u2: exp.Expr = exp.Rand()
2894        else:
2895            # Seeded: derive two values using HASH with different inputs
2896            seed = gen.this if isinstance(gen, exp.Rand) else gen
2897            u1 = exp.replace_placeholders(self.SEEDED_RANDOM_TEMPLATE, seed=seed)
2898            u2 = exp.replace_placeholders(
2899                self.SEEDED_RANDOM_TEMPLATE,
2900                seed=exp.Add(this=seed.copy(), expression=exp.Literal.number(1)),
2901            )
2902
2903        replacements = {"mean": mean, "stddev": stddev, "u1": u1, "u2": u2}
2904        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:
2906    def uniform_sql(self, expression: exp.Uniform) -> str:
2907        """
2908        Transpile Snowflake's UNIFORM(min, max, gen) to DuckDB.
2909
2910        UNIFORM returns a random value in [min, max]:
2911        - Integer result if both min and max are integers
2912        - Float result if either min or max is a float
2913        """
2914        min_val = expression.this
2915        max_val = expression.expression
2916        gen = expression.args.get("gen")
2917
2918        # Determine if result should be integer (both bounds are integers).
2919        # We do this to emulate Snowflake's behavior, INT -> INT, FLOAT -> FLOAT
2920        is_int_result = min_val.is_int and max_val.is_int
2921
2922        # Build the random value expression [0, 1)
2923        if not isinstance(gen, exp.Rand):
2924            # Seed value: (ABS(HASH(seed)) % 1000000) / 1000000.0
2925            random_expr: exp.Expr = exp.Div(
2926                this=exp.Paren(
2927                    this=exp.Mod(
2928                        this=exp.Abs(this=exp.Anonymous(this="HASH", expressions=[gen])),
2929                        expression=exp.Literal.number(1000000),
2930                    )
2931                ),
2932                expression=exp.Literal.number(1000000.0),
2933            )
2934        else:
2935            random_expr = exp.Rand()
2936
2937        # Build: min + random * (max - min [+ 1 for int])
2938        range_expr: exp.Expr = exp.Sub(this=max_val, expression=min_val)
2939        if is_int_result:
2940            range_expr = exp.Add(this=range_expr, expression=exp.Literal.number(1))
2941
2942        result: exp.Expr = exp.Add(
2943            this=min_val,
2944            expression=exp.Mul(this=random_expr, expression=exp.Paren(this=range_expr)),
2945        )
2946
2947        if is_int_result:
2948            result = exp.Cast(this=exp.Floor(this=result), to=exp.DType.BIGINT.into_expr())
2949
2950        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:
2952    def timefromparts_sql(self, expression: exp.TimeFromParts) -> str:
2953        nano = expression.args.get("nano")
2954        overflow = expression.args.get("overflow")
2955
2956        # Snowflake's TIME_FROM_PARTS supports overflow
2957        if overflow:
2958            hour = expression.args["hour"]
2959            minute = expression.args["min"]
2960            sec = expression.args["sec"]
2961
2962            # Check if values are within normal ranges - use MAKE_TIME for efficiency
2963            if not nano and all(arg.is_int for arg in [hour, minute, sec]):
2964                try:
2965                    h_val = hour.to_py()
2966                    m_val = minute.to_py()
2967                    s_val = sec.to_py()
2968                    if 0 <= h_val <= 23 and 0 <= m_val <= 59 and 0 <= s_val <= 59:
2969                        return rename_func("MAKE_TIME")(self, expression)
2970                except ValueError:
2971                    pass
2972
2973            # Overflow or nanoseconds detected - use INTERVAL arithmetic
2974            if nano:
2975                sec = sec + nano.pop() / exp.Literal.number(1000000000.0)
2976
2977            total_seconds = hour * exp.Literal.number(3600) + minute * exp.Literal.number(60) + sec
2978
2979            return self.sql(
2980                exp.Add(
2981                    this=exp.Cast(
2982                        this=exp.Literal.string("00:00:00"), to=exp.DType.TIME.into_expr()
2983                    ),
2984                    expression=exp.Interval(this=total_seconds, unit=exp.var("SECOND")),
2985                )
2986            )
2987
2988        # Default: MAKE_TIME
2989        if nano:
2990            expression.set(
2991                "sec", expression.args["sec"] + nano.pop() / exp.Literal.number(1000000000.0)
2992            )
2993
2994        return rename_func("MAKE_TIME")(self, expression)
def extract_sql(self, expression: sqlglot.expressions.temporal.Extract) -> str:
2996    def extract_sql(self, expression: exp.Extract) -> str:
2997        """
2998        Transpile EXTRACT/DATE_PART for DuckDB, handling specifiers not natively supported.
2999
3000        DuckDB doesn't support: WEEKISO, YEAROFWEEK, YEAROFWEEKISO, NANOSECOND,
3001        EPOCH_SECOND (as integer), EPOCH_MILLISECOND, EPOCH_MICROSECOND, EPOCH_NANOSECOND
3002        """
3003        this = expression.this
3004        datetime_expr = expression.expression
3005
3006        # TIMESTAMPTZ extractions may produce different results between Snowflake and DuckDB
3007        # because Snowflake applies server timezone while DuckDB uses local timezone
3008        if datetime_expr.is_type(exp.DType.TIMESTAMPTZ, exp.DType.TIMESTAMPLTZ):
3009            self.unsupported(
3010                "EXTRACT from TIMESTAMPTZ / TIMESTAMPLTZ may produce different results due to timezone handling differences"
3011            )
3012
3013        part_name = this.name.upper()
3014
3015        if part_name in self.EXTRACT_STRFTIME_MAPPINGS:
3016            fmt, cast_type = self.EXTRACT_STRFTIME_MAPPINGS[part_name]
3017
3018            # Problem: strftime doesn't accept TIME and there's no NANOSECOND function
3019            # So, for NANOSECOND with TIME, fallback to MICROSECOND * 1000
3020            is_nano_time = part_name == "NANOSECOND" and datetime_expr.is_type(
3021                exp.DType.TIME, exp.DType.TIMETZ
3022            )
3023
3024            if is_nano_time:
3025                self.unsupported("Parameter NANOSECOND is not supported with TIME type in DuckDB")
3026                return self.sql(
3027                    exp.cast(
3028                        exp.Mul(
3029                            this=exp.Extract(this=exp.var("MICROSECOND"), expression=datetime_expr),
3030                            expression=exp.Literal.number(1000),
3031                        ),
3032                        exp.DataType.from_str(cast_type, dialect="duckdb"),
3033                    )
3034                )
3035
3036            # For NANOSECOND, cast to TIMESTAMP_NS to preserve nanosecond precision
3037            strftime_input = datetime_expr
3038            if part_name == "NANOSECOND":
3039                strftime_input = exp.cast(datetime_expr, exp.DType.TIMESTAMP_NS)
3040
3041            return self.sql(
3042                exp.cast(
3043                    exp.Anonymous(
3044                        this="STRFTIME",
3045                        expressions=[strftime_input, exp.Literal.string(fmt)],
3046                    ),
3047                    exp.DataType.from_str(cast_type, dialect="duckdb"),
3048                )
3049            )
3050
3051        if part_name in self.EXTRACT_EPOCH_MAPPINGS:
3052            func_name = self.EXTRACT_EPOCH_MAPPINGS[part_name]
3053            result: exp.Expr = exp.Anonymous(this=func_name, expressions=[datetime_expr])
3054            # EPOCH returns float, cast to BIGINT for integer result
3055            if part_name == "EPOCH_SECOND":
3056                result = exp.cast(result, exp.DataType.from_str("BIGINT", dialect="duckdb"))
3057            return self.sql(result)
3058
3059        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:
3061    def timestampfromparts_sql(self, expression: exp.TimestampFromParts) -> str:
3062        # Check if this is the date/time expression form: TIMESTAMP_FROM_PARTS(date_expr, time_expr)
3063        date_expr = expression.this
3064        time_expr = expression.expression
3065
3066        if date_expr is not None and time_expr is not None:
3067            # In DuckDB, DATE + TIME produces TIMESTAMP
3068            return self.sql(exp.Add(this=date_expr, expression=time_expr))
3069
3070        # Component-based form: TIMESTAMP_FROM_PARTS(year, month, day, hour, minute, second, ...)
3071        sec = expression.args.get("sec")
3072        if sec is None:
3073            # This shouldn't happen with valid input, but handle gracefully
3074            return rename_func("MAKE_TIMESTAMP")(self, expression)
3075
3076        milli = expression.args.get("milli")
3077        if milli is not None:
3078            sec += milli.pop() / exp.Literal.number(1000.0)
3079
3080        nano = expression.args.get("nano")
3081        if nano is not None:
3082            sec += nano.pop() / exp.Literal.number(1000000000.0)
3083
3084        if milli or nano:
3085            expression.set("sec", sec)
3086
3087        return rename_func("MAKE_TIMESTAMP")(self, expression)
@unsupported_args('nano')
def timestampltzfromparts_sql( self, expression: sqlglot.expressions.temporal.TimestampLtzFromParts) -> str:
3089    @unsupported_args("nano")
3090    def timestampltzfromparts_sql(self, expression: exp.TimestampLtzFromParts) -> str:
3091        # Pop nano so rename_func only passes args that MAKE_TIMESTAMP accepts
3092        if nano := expression.args.get("nano"):
3093            nano.pop()
3094
3095        timestamp = rename_func("MAKE_TIMESTAMP")(self, expression)
3096        return f"CAST({timestamp} AS TIMESTAMPTZ)"
@unsupported_args('nano')
def timestamptzfromparts_sql( self, expression: sqlglot.expressions.temporal.TimestampTzFromParts) -> str:
3098    @unsupported_args("nano")
3099    def timestamptzfromparts_sql(self, expression: exp.TimestampTzFromParts) -> str:
3100        # Extract zone before popping
3101        zone = expression.args.get("zone")
3102        # Pop zone and nano so rename_func only passes args that MAKE_TIMESTAMP accepts
3103        if zone:
3104            zone = zone.pop()
3105
3106        if nano := expression.args.get("nano"):
3107            nano.pop()
3108
3109        timestamp = rename_func("MAKE_TIMESTAMP")(self, expression)
3110
3111        if zone:
3112            # Use AT TIME ZONE to apply the explicit timezone
3113            return f"{timestamp} AT TIME ZONE {self.sql(zone)}"
3114
3115        return timestamp
def tablesample_sql( self, expression: sqlglot.expressions.query.TableSample, tablesample_keyword: str | None = None) -> str:
3117    def tablesample_sql(
3118        self,
3119        expression: exp.TableSample,
3120        tablesample_keyword: str | None = None,
3121    ) -> str:
3122        if not isinstance(expression.parent, exp.Select):
3123            # This sample clause only applies to a single source, not the entire resulting relation
3124            tablesample_keyword = "TABLESAMPLE"
3125
3126        if expression.args.get("size"):
3127            method = expression.args.get("method")
3128            if method and method.name.upper() != "RESERVOIR":
3129                self.unsupported(
3130                    f"Sampling method {method} is not supported with a discrete sample count, "
3131                    "defaulting to reservoir sampling"
3132                )
3133                expression.set("method", exp.var("RESERVOIR"))
3134
3135        return super().tablesample_sql(expression, tablesample_keyword=tablesample_keyword)
def join_sql(self, expression: sqlglot.expressions.query.Join) -> str:
3137    def join_sql(self, expression: exp.Join) -> str:
3138        if (
3139            not expression.args.get("using")
3140            and not expression.args.get("on")
3141            and not expression.method
3142            and (expression.kind in ("", "INNER", "OUTER"))
3143        ):
3144            # Some dialects support `LEFT/INNER JOIN UNNEST(...)` without an explicit ON clause
3145            # DuckDB doesn't, but we can just add a dummy ON clause that is always true
3146            if isinstance(expression.this, exp.Unnest):
3147                return super().join_sql(expression.on(exp.true()))
3148
3149            expression.set("side", None)
3150            expression.set("kind", None)
3151
3152        return super().join_sql(expression)
def countif_sql(self, expression: sqlglot.expressions.aggregate.CountIf) -> str:
3154    def countif_sql(self, expression: exp.CountIf) -> str:
3155        if self.dialect.version >= (1, 2):
3156            return self.function_fallback_sql(expression)
3157
3158        # https://github.com/tobymao/sqlglot/pull/4749
3159        return count_if_to_sum(self, expression)
def bracket_sql(self, expression: sqlglot.expressions.core.Bracket) -> str:
3161    def bracket_sql(self, expression: exp.Bracket) -> str:
3162        if self.dialect.version >= (1, 2):
3163            return super().bracket_sql(expression)
3164
3165        # https://duckdb.org/2025/02/05/announcing-duckdb-120.html#breaking-changes
3166        this = expression.this
3167        if isinstance(this, exp.Array):
3168            this.replace(exp.paren(this))
3169
3170        bracket = super().bracket_sql(expression)
3171
3172        if not expression.args.get("returns_list_for_maps"):
3173            if not this.type:
3174                from sqlglot.optimizer.annotate_types import annotate_types
3175
3176                this = annotate_types(this, dialect=self.dialect)
3177
3178            if this.is_type(exp.DType.MAP):
3179                bracket = f"({bracket})[1]"
3180
3181        return bracket
def withingroup_sql(self, expression: sqlglot.expressions.core.WithinGroup) -> str:
3183    def withingroup_sql(self, expression: exp.WithinGroup) -> str:
3184        func = expression.this
3185
3186        # For ARRAY_AGG, DuckDB requires ORDER BY inside the function, not in WITHIN GROUP
3187        # Transform: ARRAY_AGG(x) WITHIN GROUP (ORDER BY y) -> ARRAY_AGG(x ORDER BY y)
3188        if isinstance(func, exp.ArrayAgg):
3189            if not isinstance(order := expression.expression, exp.Order):
3190                return self.sql(func)
3191
3192            # Save the original column for FILTER clause (before wrapping with Order)
3193            original_this = func.this
3194
3195            # Move ORDER BY inside ARRAY_AGG by wrapping its argument with Order
3196            # ArrayAgg.this should become Order(this=ArrayAgg.this, expressions=order.expressions)
3197            func.set(
3198                "this",
3199                exp.Order(
3200                    this=func.this.copy(),
3201                    expressions=order.expressions,
3202                ),
3203            )
3204
3205            # Generate the ARRAY_AGG function with ORDER BY and add FILTER clause if needed
3206            # Use original_this (not the Order-wrapped version) for the FILTER condition
3207            array_agg_sql = self.function_fallback_sql(func)
3208            return self._add_arrayagg_null_filter(array_agg_sql, func, original_this)
3209
3210        # For other functions (like PERCENTILES), use existing logic
3211        expression_sql = self.sql(expression, "expression")
3212
3213        if isinstance(func, exp.PERCENTILES):
3214            # Make the order key the first arg and slide the fraction to the right
3215            # https://duckdb.org/docs/sql/aggregates#ordered-set-aggregate-functions
3216            order_col = expression.find(exp.Ordered)
3217            if order_col:
3218                func.set("expression", func.this)
3219                func.set("this", order_col.this)
3220
3221        this = self.sql(expression, "this").rstrip(")")
3222
3223        return f"{this}{expression_sql})"
def length_sql(self, expression: sqlglot.expressions.string.Length) -> str:
3225    def length_sql(self, expression: exp.Length) -> str:
3226        arg = expression.this
3227
3228        # Dialects like BQ and Snowflake also accept binary values as args, so
3229        # DDB will attempt to infer the type or resort to case/when resolution
3230        if not expression.args.get("binary") or arg.is_string:
3231            return self.func("LENGTH", arg)
3232
3233        if not arg.type:
3234            from sqlglot.optimizer.annotate_types import annotate_types
3235
3236            arg = annotate_types(arg, dialect=self.dialect)
3237
3238        if arg.is_type(*exp.DataType.TEXT_TYPES):
3239            return self.func("LENGTH", arg)
3240
3241        # We need these casts to make duckdb's static type checker happy
3242        blob = exp.cast(arg, exp.DType.VARBINARY)
3243        varchar = exp.cast(arg, exp.DType.VARCHAR)
3244
3245        case = (
3246            exp.case(exp.Anonymous(this="TYPEOF", expressions=[arg]))
3247            .when(exp.Literal.string("BLOB"), exp.ByteLength(this=blob))
3248            .else_(exp.Anonymous(this="LENGTH", expressions=[varchar]))
3249        )
3250        return self.sql(case)
def bitlength_sql(self, expression: sqlglot.expressions.string.BitLength) -> str:
3252    def bitlength_sql(self, expression: exp.BitLength) -> str:
3253        if not _is_binary(arg := expression.this):
3254            return self.func("BIT_LENGTH", arg)
3255
3256        blob = exp.cast(arg, exp.DataType.Type.VARBINARY)
3257        return self.sql(exp.ByteLength(this=blob) * exp.Literal.number(8))
def chr_sql( self, expression: sqlglot.expressions.string.Chr, name: str = 'CHR') -> str:
3259    def chr_sql(self, expression: exp.Chr, name: str = "CHR") -> str:
3260        arg = expression.expressions[0]
3261        if arg.is_type(*exp.DataType.REAL_TYPES):
3262            arg = exp.cast(arg, exp.DType.INT)
3263        return self.func("CHR", arg)
def collation_sql(self, expression: sqlglot.expressions.functions.Collation) -> str:
3265    def collation_sql(self, expression: exp.Collation) -> str:
3266        self.unsupported("COLLATION function is not supported by DuckDB")
3267        return self.function_fallback_sql(expression)
def collate_sql(self, expression: sqlglot.expressions.functions.Collate) -> str:
3269    def collate_sql(self, expression: exp.Collate) -> str:
3270        if not expression.expression.is_string:
3271            return super().collate_sql(expression)
3272
3273        raw = expression.expression.name
3274        if not raw:
3275            return self.sql(expression.this)
3276
3277        parts = []
3278        for part in raw.split("-"):
3279            lower = part.lower()
3280            if lower not in _SNOWFLAKE_COLLATION_DEFAULTS:
3281                if lower in _SNOWFLAKE_COLLATION_UNSUPPORTED:
3282                    self.unsupported(
3283                        f"Snowflake collation specifier '{part}' has no DuckDB equivalent"
3284                    )
3285                parts.append(lower)
3286
3287        if not parts:
3288            return self.sql(expression.this)
3289        return super().collate_sql(
3290            exp.Collate(this=expression.this, expression=exp.var(".".join(parts)))
3291        )
def regexpcount_sql(self, expression: sqlglot.expressions.string.RegexpCount) -> str:
3323    def regexpcount_sql(self, expression: exp.RegexpCount) -> str:
3324        this = expression.this
3325        pattern = expression.expression
3326        position = expression.args.get("position")
3327        parameters = expression.args.get("parameters")
3328
3329        # Validate flags - only "ims" flags are supported for embedded patterns
3330        validated_flags = self._validate_regexp_flags(parameters, supported_flags="ims")
3331
3332        if position:
3333            this = exp.Substring(this=this, start=position)
3334
3335        # Embed flags in pattern (REGEXP_EXTRACT_ALL doesn't support flags argument)
3336        if validated_flags:
3337            pattern = exp.Concat(expressions=[exp.Literal.string(f"(?{validated_flags})"), pattern])
3338
3339        # Handle empty pattern: Snowflake returns 0, DuckDB would match between every character
3340        result = (
3341            exp.case()
3342            .when(
3343                exp.EQ(this=pattern, expression=exp.Literal.string("")),
3344                exp.Literal.number(0),
3345            )
3346            .else_(
3347                exp.Length(
3348                    this=exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern])
3349                )
3350            )
3351        )
3352
3353        return self.sql(result)
def regexpreplace_sql(self, expression: sqlglot.expressions.string.RegexpReplace) -> str:
3355    def regexpreplace_sql(self, expression: exp.RegexpReplace) -> str:
3356        subject = expression.this
3357        pattern = expression.expression
3358        replacement = expression.args.get("replacement") or exp.Literal.string("")
3359        position = expression.args.get("position")
3360        occurrence = expression.args.get("occurrence")
3361        modifiers = expression.args.get("modifiers")
3362
3363        validated_flags = self._validate_regexp_flags(modifiers, supported_flags="cimsg") or ""
3364
3365        # Handle occurrence (only literals supported)
3366        if occurrence and not occurrence.is_int:
3367            self.unsupported("REGEXP_REPLACE with non-literal occurrence")
3368        else:
3369            occurrence = occurrence.to_py() if occurrence and occurrence.is_int else 0
3370            if occurrence > 1:
3371                self.unsupported(f"REGEXP_REPLACE occurrence={occurrence} not supported")
3372            # flag duckdb to do either all or none, single_replace check is for duckdb round trip
3373            elif (
3374                occurrence == 0
3375                and "g" not in validated_flags
3376                and not expression.args.get("single_replace")
3377            ):
3378                validated_flags += "g"
3379
3380        # Handle position (only literals supported)
3381        prefix = None
3382        if position and not position.is_int:
3383            self.unsupported("REGEXP_REPLACE with non-literal position")
3384        elif position and position.is_int and position.to_py() > 1:
3385            pos = position.to_py()
3386            prefix = exp.Substring(
3387                this=subject, start=exp.Literal.number(1), length=exp.Literal.number(pos - 1)
3388            )
3389            subject = exp.Substring(this=subject, start=exp.Literal.number(pos))
3390
3391        result: exp.Expr = exp.Anonymous(
3392            this="REGEXP_REPLACE",
3393            expressions=[
3394                subject,
3395                pattern,
3396                replacement,
3397                exp.Literal.string(validated_flags) if validated_flags else None,
3398            ],
3399        )
3400
3401        if prefix:
3402            result = exp.Concat(expressions=[prefix, result])
3403
3404        return self.sql(result)
def regexplike_sql(self, expression: sqlglot.expressions.core.RegexpLike) -> str:
3406    def regexplike_sql(self, expression: exp.RegexpLike) -> str:
3407        this = expression.this
3408        pattern = expression.expression
3409        flag = expression.args.get("flag")
3410
3411        if expression.args.get("full_match"):
3412            validated_flags = self._validate_regexp_flags(flag, supported_flags="cims")
3413            flag = exp.Literal.string(validated_flags) if validated_flags else None
3414            return self.func("REGEXP_FULL_MATCH", this, pattern, flag)
3415
3416        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:
3418    @unsupported_args("ins_cost", "del_cost", "sub_cost")
3419    def levenshtein_sql(self, expression: exp.Levenshtein) -> str:
3420        this = expression.this
3421        expr = expression.expression
3422        max_dist = expression.args.get("max_dist")
3423
3424        if max_dist is None:
3425            return self.func("LEVENSHTEIN", this, expr)
3426
3427        # Emulate Snowflake semantics: if distance > max_dist, return max_dist
3428        levenshtein = exp.Levenshtein(this=this, expression=expr)
3429        return self.sql(exp.Least(this=levenshtein, expressions=[max_dist]))
def pad_sql(self, expression: sqlglot.expressions.string.Pad) -> str:
3431    def pad_sql(self, expression: exp.Pad) -> str:
3432        """
3433        Handle RPAD/LPAD for VARCHAR and BINARY types.
3434
3435        For VARCHAR: Delegate to parent class
3436        For BINARY: Lower to: input || REPEAT(pad, GREATEST(0, target_len - OCTET_LENGTH(input)))
3437        """
3438        string_arg = expression.this
3439        fill_arg = expression.args.get("fill_pattern") or exp.Literal.string(" ")
3440
3441        if _is_binary(string_arg) or _is_binary(fill_arg):
3442            length_arg = expression.expression
3443            is_left = expression.args.get("is_left")
3444
3445            input_len = exp.ByteLength(this=string_arg)
3446            chars_needed = length_arg - input_len
3447            pad_count = exp.Greatest(
3448                this=exp.Literal.number(0), expressions=[chars_needed], ignore_nulls=True
3449            )
3450            repeat_expr = exp.Repeat(this=fill_arg, times=pad_count)
3451
3452            left, right = string_arg, repeat_expr
3453            if is_left:
3454                left, right = right, left
3455
3456            result = exp.DPipe(this=left, expression=right)
3457            return self.sql(result)
3458
3459        # For VARCHAR: Delegate to parent class (handles PAD_FILL_PATTERN_IS_REQUIRED)
3460        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:
3462    def minhash_sql(self, expression: exp.Minhash) -> str:
3463        k = expression.this
3464        exprs = expression.expressions
3465
3466        if len(exprs) != 1 or isinstance(exprs[0], exp.Star):
3467            self.unsupported(
3468                "MINHASH with multiple expressions or * requires manual query restructuring"
3469            )
3470            return self.func("MINHASH", k, *exprs)
3471
3472        expr = exprs[0]
3473        result = exp.replace_placeholders(self.MINHASH_TEMPLATE.copy(), expr=expr, k=k)
3474        return f"({self.sql(result)})"
def minhashcombine_sql(self, expression: sqlglot.expressions.aggregate.MinhashCombine) -> str:
3476    def minhashcombine_sql(self, expression: exp.MinhashCombine) -> str:
3477        expr = expression.this
3478        result = exp.replace_placeholders(self.MINHASH_COMBINE_TEMPLATE.copy(), expr=expr)
3479        return f"({self.sql(result)})"
def approximatesimilarity_sql( self, expression: sqlglot.expressions.aggregate.ApproximateSimilarity) -> str:
3481    def approximatesimilarity_sql(self, expression: exp.ApproximateSimilarity) -> str:
3482        expr = expression.this
3483        result = exp.replace_placeholders(self.APPROXIMATE_SIMILARITY_TEMPLATE.copy(), expr=expr)
3484        return f"({self.sql(result)})"
def arrayuniqueagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayUniqueAgg) -> str:
3486    def arrayuniqueagg_sql(self, expression: exp.ArrayUniqueAgg) -> str:
3487        return self.sql(
3488            exp.Filter(
3489                this=exp.func("LIST", exp.Distinct(expressions=[expression.this])),
3490                expression=exp.Where(this=expression.this.copy().is_(exp.null()).not_()),
3491            )
3492        )
def arrayunionagg_sql(self, expression: sqlglot.expressions.aggregate.ArrayUnionAgg) -> str:
3494    def arrayunionagg_sql(self, expression: exp.ArrayUnionAgg) -> str:
3495        self.unsupported("ARRAY_UNION_AGG is not supported in DuckDB")
3496        return self.function_fallback_sql(expression)
def arraydistinct_sql(self, expression: sqlglot.expressions.array.ArrayDistinct) -> str:
3498    def arraydistinct_sql(self, expression: exp.ArrayDistinct) -> str:
3499        arr = expression.this
3500        func = self.func("LIST_DISTINCT", arr)
3501
3502        if expression.args.get("check_null"):
3503            add_null_to_array = exp.func(
3504                "LIST_APPEND", exp.func("LIST_DISTINCT", exp.ArrayCompact(this=arr)), exp.Null()
3505            )
3506            return self.sql(
3507                exp.If(
3508                    this=exp.NEQ(
3509                        this=exp.ArraySize(this=arr), expression=exp.func("LIST_COUNT", arr)
3510                    ),
3511                    true=add_null_to_array,
3512                    false=func,
3513                )
3514            )
3515
3516        return func
def arrayintersect_sql(self, expression: sqlglot.expressions.array.ArrayIntersect) -> str:
3518    def arrayintersect_sql(self, expression: exp.ArrayIntersect) -> str:
3519        if expression.args.get("is_multiset") and len(expression.expressions) == 2:
3520            return self._array_bag_sql(
3521                self.ARRAY_INTERSECTION_CONDITION,
3522                expression.expressions[0],
3523                expression.expressions[1],
3524            )
3525        return self.function_fallback_sql(expression)
def arrayexcept_sql(self, expression: sqlglot.expressions.array.ArrayExcept) -> str:
3527    def arrayexcept_sql(self, expression: exp.ArrayExcept) -> str:
3528        arr1, arr2 = expression.this, expression.expression
3529        if expression.args.get("is_multiset"):
3530            return self._array_bag_sql(self.ARRAY_EXCEPT_CONDITION, arr1, arr2)
3531        return self.sql(
3532            exp.replace_placeholders(self.ARRAY_EXCEPT_SET_TEMPLATE, arr1=arr1, arr2=arr2)
3533        )
def arrayslice_sql(self, expression: sqlglot.expressions.array.ArraySlice) -> str:
3535    def arrayslice_sql(self, expression: exp.ArraySlice) -> str:
3536        """
3537        Transpiles Snowflake's ARRAY_SLICE (0-indexed, exclusive end) to DuckDB's
3538        ARRAY_SLICE (1-indexed, inclusive end) by wrapping start and end in CASE
3539        expressions that adjust the index at query time:
3540          - start: CASE WHEN start >= 0 THEN start + 1 ELSE start END
3541          - end:   CASE WHEN end < 0 THEN end - 1 ELSE end END
3542        """
3543        start, end = expression.args.get("start"), expression.args.get("end")
3544
3545        if expression.args.get("zero_based"):
3546            if start is not None:
3547                start = (
3548                    exp.case()
3549                    .when(
3550                        exp.GTE(this=start.copy(), expression=exp.Literal.number(0)),
3551                        exp.Add(this=start.copy(), expression=exp.Literal.number(1)),
3552                    )
3553                    .else_(start)
3554                )
3555            if end is not None:
3556                end = (
3557                    exp.case()
3558                    .when(
3559                        exp.LT(this=end.copy(), expression=exp.Literal.number(0)),
3560                        exp.Sub(this=end.copy(), expression=exp.Literal.number(1)),
3561                    )
3562                    .else_(end)
3563                )
3564
3565        return self.func("ARRAY_SLICE", expression.this, start, end, expression.args.get("step"))

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

  • start: CASE WHEN start >= 0 THEN start + 1 ELSE start END
  • end: CASE WHEN end < 0 THEN end - 1 ELSE end END
def arrayszip_sql(self, expression: sqlglot.expressions.array.ArraysZip) -> str:
3567    def arrayszip_sql(self, expression: exp.ArraysZip) -> str:
3568        args = expression.expressions
3569
3570        if not args:
3571            # Return [{}] - using MAP([], []) since DuckDB can't represent empty structs
3572            return self.sql(exp.array(exp.Map(keys=exp.array(), values=exp.array())))
3573
3574        # Build placeholder values for template
3575        lengths = [exp.Length(this=arg) for arg in args]
3576        max_len = (
3577            lengths[0]
3578            if len(lengths) == 1
3579            else exp.Greatest(this=lengths[0], expressions=lengths[1:])
3580        )
3581
3582        # Empty struct with same schema: {'$1': NULL, '$2': NULL, ...}
3583        empty_struct = exp.func(
3584            "STRUCT",
3585            *[
3586                exp.PropertyEQ(this=exp.Literal.string(f"${i + 1}"), expression=exp.Null())
3587                for i in range(len(args))
3588            ],
3589        )
3590
3591        # Struct for transform: {'$1': COALESCE(arr1, [])[__i + 1], ...}
3592        # COALESCE wrapping handles NULL arrays - prevents invalid NULL[i] syntax
3593        index = exp.column("__i") + 1
3594        transform_struct = exp.func(
3595            "STRUCT",
3596            *[
3597                exp.PropertyEQ(
3598                    this=exp.Literal.string(f"${i + 1}"),
3599                    expression=exp.func("COALESCE", arg, exp.array())[index],
3600                )
3601                for i, arg in enumerate(args)
3602            ],
3603        )
3604
3605        result = exp.replace_placeholders(
3606            self.ARRAYS_ZIP_TEMPLATE.copy(),
3607            null_check=exp.or_(*[arg.is_(exp.Null()) for arg in args]),
3608            all_empty_check=exp.and_(
3609                *[
3610                    exp.EQ(this=exp.Length(this=arg), expression=exp.Literal.number(0))
3611                    for arg in args
3612                ]
3613            ),
3614            empty_struct=empty_struct,
3615            max_len=max_len,
3616            transform_struct=transform_struct,
3617        )
3618        return self.sql(result)
def lower_sql(self, expression: sqlglot.expressions.string.Lower) -> str:
3620    def lower_sql(self, expression: exp.Lower) -> str:
3621        result_sql = self.func("LOWER", _cast_to_varchar(expression.this))
3622        return _gen_with_cast_to_blob(self, expression, result_sql)
def upper_sql(self, expression: sqlglot.expressions.string.Upper) -> str:
3624    def upper_sql(self, expression: exp.Upper) -> str:
3625        result_sql = self.func("UPPER", _cast_to_varchar(expression.this))
3626        return _gen_with_cast_to_blob(self, expression, result_sql)
def reverse_sql(self, expression: sqlglot.expressions.string.Reverse) -> str:
3628    def reverse_sql(self, expression: exp.Reverse) -> str:
3629        result_sql = self.func("REVERSE", _cast_to_varchar(expression.this))
3630        return _gen_with_cast_to_blob(self, expression, result_sql)
def left_sql(self, expression: sqlglot.expressions.string.Left) -> str:
3656    def left_sql(self, expression: exp.Left) -> str:
3657        return self._left_right_sql(expression, "LEFT")
def right_sql(self, expression: sqlglot.expressions.string.Right) -> str:
3659    def right_sql(self, expression: exp.Right) -> str:
3660        return self._left_right_sql(expression, "RIGHT")
def rtrimmedlength_sql(self, expression: sqlglot.expressions.string.RtrimmedLength) -> str:
3662    def rtrimmedlength_sql(self, expression: exp.RtrimmedLength) -> str:
3663        return self.func("LENGTH", exp.Trim(this=expression.this, position="TRAILING"))
def stuff_sql(self, expression: sqlglot.expressions.string.Stuff) -> str:
3665    def stuff_sql(self, expression: exp.Stuff) -> str:
3666        base = expression.this
3667        start = expression.args["start"]
3668        length = expression.args["length"]
3669        insertion = expression.expression
3670        is_binary = _is_binary(base)
3671
3672        if is_binary:
3673            # DuckDB's SUBSTRING doesn't accept BLOB; operate on the HEX string instead
3674            # (each byte = 2 hex chars), then UNHEX back to BLOB
3675            base = exp.Hex(this=base)
3676            insertion = exp.Hex(this=insertion)
3677            left = exp.Substring(
3678                this=base.copy(),
3679                start=exp.Literal.number(1),
3680                length=(start.copy() - exp.Literal.number(1)) * exp.Literal.number(2),
3681            )
3682            right = exp.Substring(
3683                this=base.copy(),
3684                start=((start + length) - exp.Literal.number(1)) * exp.Literal.number(2)
3685                + exp.Literal.number(1),
3686            )
3687        else:
3688            left = exp.Substring(
3689                this=base.copy(),
3690                start=exp.Literal.number(1),
3691                length=start.copy() - exp.Literal.number(1),
3692            )
3693            right = exp.Substring(this=base.copy(), start=start + length)
3694        result: exp.Expr = exp.DPipe(
3695            this=exp.DPipe(this=left, expression=insertion), expression=right
3696        )
3697
3698        if is_binary:
3699            result = exp.Unhex(this=result)
3700
3701        return self.sql(result)
def rand_sql(self, expression: sqlglot.expressions.functions.Rand) -> str:
3703    def rand_sql(self, expression: exp.Rand) -> str:
3704        seed = expression.this
3705        if seed is not None:
3706            self.unsupported("RANDOM with seed is not supported in DuckDB")
3707
3708        lower = expression.args.get("lower")
3709        upper = expression.args.get("upper")
3710
3711        if lower and upper:
3712            # scale DuckDB's [0,1) to the specified range
3713            range_size = exp.paren(upper - lower)
3714            scaled = exp.Add(this=lower, expression=exp.func("random") * range_size)
3715
3716            # For now we assume that if bounds are set, return type is BIGINT. Snowflake/Teradata
3717            result = exp.cast(scaled, exp.DType.BIGINT)
3718            return self.sql(result)
3719
3720        # Default DuckDB behavior - just return RANDOM() as float
3721        return "RANDOM()"
def bytelength_sql(self, expression: sqlglot.expressions.string.ByteLength) -> str:
3723    def bytelength_sql(self, expression: exp.ByteLength) -> str:
3724        arg = expression.this
3725
3726        # Check if it's a text type (handles both literals and annotated expressions)
3727        if arg.is_type(*exp.DataType.TEXT_TYPES):
3728            return self.func("OCTET_LENGTH", exp.Encode(this=arg))
3729
3730        # Default: pass through as-is (conservative for DuckDB, handles binary and unannotated)
3731        return self.func("OCTET_LENGTH", arg)
def base64encode_sql(self, expression: sqlglot.expressions.string.Base64Encode) -> str:
3733    def base64encode_sql(self, expression: exp.Base64Encode) -> str:
3734        # DuckDB TO_BASE64 requires BLOB input
3735        # Snowflake BASE64_ENCODE accepts both VARCHAR and BINARY - for VARCHAR it implicitly
3736        # encodes UTF-8 bytes. We add ENCODE unless the input is a binary type.
3737        result = expression.this
3738
3739        # Check if input is a string type - ENCODE only accepts VARCHAR
3740        if result.is_type(*exp.DataType.TEXT_TYPES):
3741            result = exp.Encode(this=result)
3742
3743        result = exp.ToBase64(this=result)
3744
3745        max_line_length = expression.args.get("max_line_length")
3746        alphabet = expression.args.get("alphabet")
3747
3748        # Handle custom alphabet by replacing standard chars with custom ones
3749        result = _apply_base64_alphabet_replacements(result, alphabet)
3750
3751        # Handle max_line_length by inserting newlines every N characters
3752        line_length = (
3753            t.cast(int, max_line_length.to_py())
3754            if isinstance(max_line_length, exp.Literal) and max_line_length.is_number
3755            else 0
3756        )
3757        if line_length > 0:
3758            newline = exp.Chr(expressions=[exp.Literal.number(10)])
3759            result = exp.Trim(
3760                this=exp.RegexpReplace(
3761                    this=result,
3762                    expression=exp.Literal.string(f"(.{{{line_length}}})"),
3763                    replacement=exp.Concat(expressions=[exp.Literal.string("\\1"), newline.copy()]),
3764                ),
3765                expression=newline,
3766                position="TRAILING",
3767            )
3768
3769        return self.sql(result)
def hex_sql(self, expression: sqlglot.expressions.string.Hex) -> str:
3771    def hex_sql(self, expression: exp.Hex) -> str:
3772        case = expression.args.get("case")
3773
3774        if not case:
3775            return self.func("HEX", expression.this)
3776
3777        hex_expr = exp.Hex(this=expression.this)
3778        return self.sql(
3779            exp.case()
3780            .when(case.is_(exp.null()), exp.null())
3781            .when(case.copy().eq(0), exp.Lower(this=hex_expr.copy()))
3782            .else_(hex_expr)
3783        )
def replace_sql(self, expression: sqlglot.expressions.string.Replace) -> str:
3785    def replace_sql(self, expression: exp.Replace) -> str:
3786        result_sql = self.func(
3787            "REPLACE",
3788            _cast_to_varchar(expression.this),
3789            _cast_to_varchar(expression.expression),
3790            _cast_to_varchar(expression.args.get("replacement")),
3791        )
3792        return _gen_with_cast_to_blob(self, expression, result_sql)
def bitwisexor_sql(self, expression: sqlglot.expressions.core.BitwiseXor) -> str:
3799    def bitwisexor_sql(self, expression: exp.BitwiseXor) -> str:
3800        _prepare_binary_bitwise_args(expression)
3801        result_sql = self.func("XOR", expression.this, expression.expression)
3802        return _gen_with_cast_to_blob(self, expression, result_sql)
def objectinsert_sql(self, expression: sqlglot.expressions.json.ObjectInsert) -> str:
3804    def objectinsert_sql(self, expression: exp.ObjectInsert) -> str:
3805        this = expression.this
3806        key = expression.args.get("key")
3807        key_sql = key.name if isinstance(key, exp.Expr) else ""
3808        value_sql = self.sql(expression, "value")
3809
3810        kv_sql = f"{key_sql} := {value_sql}"
3811
3812        # If the input struct is empty e.g. transpiling OBJECT_INSERT(OBJECT_CONSTRUCT(), key, value) from Snowflake
3813        # then we can generate STRUCT_PACK which will build it since STRUCT_INSERT({}, key := value) is not valid DuckDB
3814        if isinstance(this, exp.Struct) and not this.expressions:
3815            return self.func("STRUCT_PACK", kv_sql)
3816
3817        return self.func("STRUCT_INSERT", this, kv_sql)
def mapcat_sql(self, expression: sqlglot.expressions.array.MapCat) -> str:
3819    def mapcat_sql(self, expression: exp.MapCat) -> str:
3820        result = exp.replace_placeholders(
3821            self.MAPCAT_TEMPLATE.copy(),
3822            map1=expression.this,
3823            map2=expression.expression,
3824        )
3825        return self.sql(result)
def mapcontainskey_sql(self, expression: sqlglot.expressions.array.MapContainsKey) -> str:
3827    def mapcontainskey_sql(self, expression: exp.MapContainsKey) -> str:
3828        return self.func(
3829            "ARRAY_CONTAINS", exp.func("MAP_KEYS", expression.args["key"]), expression.this
3830        )
def mapdelete_sql(self, expression: sqlglot.expressions.array.MapDelete) -> str:
3832    def mapdelete_sql(self, expression: exp.MapDelete) -> str:
3833        map_arg = expression.this
3834        keys_to_delete = expression.expressions
3835
3836        x_dot_key = exp.Dot(this=exp.to_identifier("x"), expression=exp.to_identifier("key"))
3837
3838        lambda_expr = exp.Lambda(
3839            this=exp.In(this=x_dot_key, expressions=keys_to_delete).not_(),
3840            expressions=[exp.to_identifier("x")],
3841        )
3842        result = exp.func(
3843            "MAP_FROM_ENTRIES",
3844            exp.ArrayFilter(this=exp.func("MAP_ENTRIES", map_arg), expression=lambda_expr),
3845        )
3846        return self.sql(result)
def mappick_sql(self, expression: sqlglot.expressions.array.MapPick) -> str:
3848    def mappick_sql(self, expression: exp.MapPick) -> str:
3849        map_arg = expression.this
3850        keys_to_pick = expression.expressions
3851
3852        x_dot_key = exp.Dot(this=exp.to_identifier("x"), expression=exp.to_identifier("key"))
3853
3854        if len(keys_to_pick) == 1 and keys_to_pick[0].is_type(exp.DType.ARRAY):
3855            lambda_expr = exp.Lambda(
3856                this=exp.func("ARRAY_CONTAINS", keys_to_pick[0], x_dot_key),
3857                expressions=[exp.to_identifier("x")],
3858            )
3859        else:
3860            lambda_expr = exp.Lambda(
3861                this=exp.In(this=x_dot_key, expressions=keys_to_pick),
3862                expressions=[exp.to_identifier("x")],
3863            )
3864
3865        result = exp.func(
3866            "MAP_FROM_ENTRIES",
3867            exp.func("LIST_FILTER", exp.func("MAP_ENTRIES", map_arg), lambda_expr),
3868        )
3869        return self.sql(result)
def mapsize_sql(self, expression: sqlglot.expressions.array.MapSize) -> str:
3871    def mapsize_sql(self, expression: exp.MapSize) -> str:
3872        return self.func("CARDINALITY", expression.this)
@unsupported_args('update_flag')
def mapinsert_sql(self, expression: sqlglot.expressions.array.MapInsert) -> str:
3874    @unsupported_args("update_flag")
3875    def mapinsert_sql(self, expression: exp.MapInsert) -> str:
3876        map_arg = expression.this
3877        key = expression.args.get("key")
3878        value = expression.args.get("value")
3879
3880        map_type = map_arg.type
3881
3882        if value is not None:
3883            if map_type and map_type.expressions and len(map_type.expressions) > 1:
3884                # Extract the value type from MAP(key_type, value_type)
3885                value_type = map_type.expressions[1]
3886                # Cast value to match the map's value type to avoid type conflicts
3887                value = exp.cast(value, value_type)
3888            # else: polymorphic MAP case - no type parameters available, use value as-is
3889
3890        # Create a single-entry map for the new key-value pair
3891        new_entry_struct = exp.Struct(expressions=[exp.PropertyEQ(this=key, expression=value)])
3892        new_entry: exp.Expression = exp.ToMap(this=new_entry_struct)
3893
3894        # Use MAP_CONCAT to merge the original map with the new entry
3895        # This automatically handles both insert and update cases
3896        result = exp.func("MAP_CONCAT", map_arg, new_entry)
3897
3898        return self.sql(result)
def startswith_sql(self, expression: sqlglot.expressions.string.StartsWith) -> str:
3900    def startswith_sql(self, expression: exp.StartsWith) -> str:
3901        return self.func(
3902            "STARTS_WITH",
3903            _cast_to_varchar(expression.this),
3904            _cast_to_varchar(expression.expression),
3905        )
def space_sql(self, expression: sqlglot.expressions.string.Space) -> str:
3907    def space_sql(self, expression: exp.Space) -> str:
3908        # DuckDB's REPEAT requires BIGINT for the count parameter
3909        return self.sql(
3910            exp.Repeat(
3911                this=exp.Literal.string(" "),
3912                times=exp.cast(expression.this, exp.DType.BIGINT),
3913            )
3914        )
def tablefromrows_sql(self, expression: sqlglot.expressions.query.TableFromRows) -> str:
3916    def tablefromrows_sql(self, expression: exp.TableFromRows) -> str:
3917        # For GENERATOR, unwrap TABLE() - just emit the Generator (becomes RANGE)
3918        if isinstance(expression.this, exp.Generator):
3919            # Preserve alias, joins, and other table-level args
3920            table = exp.Table(
3921                this=expression.this,
3922                alias=expression.args.get("alias"),
3923                joins=expression.args.get("joins"),
3924            )
3925            return self.sql(table)
3926
3927        return super().tablefromrows_sql(expression)
def unnest_sql(self, expression: sqlglot.expressions.array.Unnest) -> str:
3929    def unnest_sql(self, expression: exp.Unnest) -> str:
3930        explode_array = expression.args.get("explode_array")
3931        if explode_array:
3932            # In BigQuery, UNNESTing a nested array leads to explosion of the top-level array & struct
3933            # This is transpiled to DDB by transforming "FROM UNNEST(...)" to "FROM (SELECT UNNEST(..., max_depth => 2))"
3934            expression.expressions.append(
3935                exp.Kwarg(this=exp.var("max_depth"), expression=exp.Literal.number(2))
3936            )
3937
3938            # If BQ's UNNEST is aliased, we transform it from a column alias to a table alias in DDB
3939            alias = expression.args.get("alias")
3940            if isinstance(alias, exp.TableAlias):
3941                expression.set("alias", None)
3942                if alias.columns:
3943                    alias = exp.TableAlias(this=seq_get(alias.columns, 0))
3944
3945            unnest_sql = super().unnest_sql(expression)
3946            select = exp.Select(expressions=[unnest_sql]).subquery(alias)
3947            return self.sql(select)
3948
3949        return super().unnest_sql(expression)
def ignorenulls_sql(self, expression: sqlglot.expressions.core.IgnoreNulls) -> str:
3951    def ignorenulls_sql(self, expression: exp.IgnoreNulls) -> str:
3952        this = expression.this
3953
3954        if isinstance(this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
3955            # DuckDB should render IGNORE NULLS only for the general-purpose
3956            # window functions that accept it e.g. FIRST_VALUE(... IGNORE NULLS) OVER (...)
3957            return super().ignorenulls_sql(expression)
3958
3959        if isinstance(this, exp.First):
3960            this = exp.AnyValue(this=this.this)
3961
3962        if not isinstance(this, (exp.AnyValue, exp.ApproxQuantiles)):
3963            self.unsupported("IGNORE NULLS is not supported for non-window functions.")
3964
3965        return self.sql(this)
def split_sql(self, expression: sqlglot.expressions.string.Split) -> str:
3967    def split_sql(self, expression: exp.Split) -> str:
3968        base_func = exp.func("STR_SPLIT", expression.this, expression.expression)
3969
3970        case_expr = exp.case().else_(base_func)
3971        needs_case = False
3972
3973        if expression.args.get("null_returns_null"):
3974            case_expr = case_expr.when(expression.expression.is_(exp.null()), exp.null())
3975            needs_case = True
3976
3977        if expression.args.get("empty_delimiter_returns_whole"):
3978            # When delimiter is empty string, return input string as single array element
3979            array_with_input = exp.array(expression.this)
3980            case_expr = case_expr.when(
3981                expression.expression.eq(exp.Literal.string("")), array_with_input
3982            )
3983            needs_case = True
3984
3985        return self.sql(case_expr if needs_case else base_func)
def splitpart_sql(self, expression: sqlglot.expressions.string.SplitPart) -> str:
3987    def splitpart_sql(self, expression: exp.SplitPart) -> str:
3988        string_arg = expression.this
3989        delimiter_arg = expression.args.get("delimiter")
3990        part_index_arg = expression.args.get("part_index")
3991
3992        if delimiter_arg and part_index_arg:
3993            # Handle Snowflake's "index 0 and 1 both return first element" behavior
3994            if expression.args.get("part_index_zero_as_one"):
3995                # Convert 0 to 1 for compatibility
3996
3997                part_index_arg = exp.Paren(
3998                    this=exp.case()
3999                    .when(part_index_arg.eq(exp.Literal.number("0")), exp.Literal.number("1"))
4000                    .else_(part_index_arg)
4001                )
4002
4003            # Use Anonymous to avoid recursion
4004            base_func_expr: exp.Expr = exp.Anonymous(
4005                this="SPLIT_PART", expressions=[string_arg, delimiter_arg, part_index_arg]
4006            )
4007            needs_case_transform = False
4008            case_expr = exp.case().else_(base_func_expr)
4009
4010            if expression.args.get("empty_delimiter_returns_whole"):
4011                # When delimiter is empty string:
4012                # - Return whole string if part_index is 1 or -1
4013                # - Return empty string otherwise
4014                empty_case = exp.Paren(
4015                    this=exp.case()
4016                    .when(
4017                        exp.or_(
4018                            part_index_arg.eq(exp.Literal.number("1")),
4019                            part_index_arg.eq(exp.Literal.number("-1")),
4020                        ),
4021                        string_arg,
4022                    )
4023                    .else_(exp.Literal.string(""))
4024                )
4025
4026                case_expr = case_expr.when(delimiter_arg.eq(exp.Literal.string("")), empty_case)
4027                needs_case_transform = True
4028
4029            """
4030            Output looks something like this:
4031
4032            CASE
4033            WHEN delimiter is '' THEN
4034                (
4035                    CASE
4036                    WHEN adjusted_part_index = 1 OR adjusted_part_index = -1 THEN input
4037                    ELSE '' END
4038                )
4039            ELSE SPLIT_PART(input, delimiter, adjusted_part_index)
4040            END
4041
4042            """
4043            return self.sql(case_expr if needs_case_transform else base_func_expr)
4044
4045        return self.function_fallback_sql(expression)
def respectnulls_sql(self, expression: sqlglot.expressions.core.RespectNulls) -> str:
4047    def respectnulls_sql(self, expression: exp.RespectNulls) -> str:
4048        if isinstance(expression.this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
4049            # DuckDB should render RESPECT NULLS only for the general-purpose
4050            # window functions that accept it e.g. FIRST_VALUE(... RESPECT NULLS) OVER (...)
4051            return super().respectnulls_sql(expression)
4052
4053        self.unsupported("RESPECT NULLS is not supported for non-window functions.")
4054        return self.sql(expression, "this")
def arraytostring_sql(self, expression: sqlglot.expressions.array.ArrayToString) -> str:
4056    def arraytostring_sql(self, expression: exp.ArrayToString) -> str:
4057        null = expression.args.get("null")
4058
4059        if expression.args.get("null_is_empty"):
4060            x = exp.to_identifier("x")
4061            list_transform = exp.Transform(
4062                this=expression.this.copy(),
4063                expression=exp.Lambda(
4064                    this=exp.Coalesce(
4065                        this=exp.cast(x, "TEXT"), expressions=[exp.Literal.string("")]
4066                    ),
4067                    expressions=[x],
4068                ),
4069            )
4070            array_to_string = exp.ArrayToString(
4071                this=list_transform, expression=expression.expression
4072            )
4073            if expression.args.get("null_delim_is_null"):
4074                return self.sql(
4075                    exp.case()
4076                    .when(expression.expression.copy().is_(exp.null()), exp.null())
4077                    .else_(array_to_string)
4078                )
4079            return self.sql(array_to_string)
4080
4081        if null:
4082            x = exp.to_identifier("x")
4083            return self.sql(
4084                exp.ArrayToString(
4085                    this=exp.Transform(
4086                        this=expression.this,
4087                        expression=exp.Lambda(
4088                            this=exp.Coalesce(this=x, expressions=[null]),
4089                            expressions=[x],
4090                        ),
4091                    ),
4092                    expression=expression.expression,
4093                )
4094            )
4095
4096        return self.func("ARRAY_TO_STRING", expression.this, expression.expression)
def concatws_sql(self, expression: sqlglot.expressions.string.ConcatWs) -> str:
4098    def concatws_sql(self, expression: exp.ConcatWs) -> str:
4099        # DuckDB-specific: handle binary types using DPipe (||) operator
4100        separator = seq_get(expression.expressions, 0)
4101        args = expression.expressions[1:]
4102
4103        if any(_is_binary(arg) for arg in [separator, *args]):
4104            result = args[0]
4105            for arg in args[1:]:
4106                result = exp.DPipe(
4107                    this=exp.DPipe(this=result, expression=separator), expression=arg
4108                )
4109            return self.sql(result)
4110
4111        return super().concatws_sql(expression)
def regexpextract_sql(self, expression: sqlglot.expressions.string.RegexpExtract) -> str:
4166    def regexpextract_sql(self, expression: exp.RegexpExtract) -> str:
4167        return self._regexp_extract_sql(expression)
def regexpextractall_sql(self, expression: sqlglot.expressions.string.RegexpExtractAll) -> str:
4169    def regexpextractall_sql(self, expression: exp.RegexpExtractAll) -> str:
4170        return self._regexp_extract_sql(expression)
def regexpinstr_sql(self, expression: sqlglot.expressions.string.RegexpInstr) -> str:
4172    def regexpinstr_sql(self, expression: exp.RegexpInstr) -> str:
4173        this = expression.this
4174        pattern = expression.expression
4175        position = expression.args.get("position")
4176        orig_occ = expression.args.get("occurrence")
4177        occurrence = orig_occ or exp.Literal.number(1)
4178        option = expression.args.get("option")
4179        parameters = expression.args.get("parameters")
4180
4181        validated_flags = self._validate_regexp_flags(parameters, supported_flags="ims")
4182        if validated_flags:
4183            pattern = exp.Concat(expressions=[exp.Literal.string(f"(?{validated_flags})"), pattern])
4184
4185        # Handle starting position offset
4186        pos_offset: exp.Expr = exp.Literal.number(0)
4187        if position and (not position.is_int or position.to_py() > 1):
4188            this = exp.Substring(this=this, start=position)
4189            pos_offset = position - exp.Literal.number(1)
4190
4191        # Helper: LIST_SUM(LIST_TRANSFORM(list[1:end], x -> LENGTH(x)))
4192        def sum_lengths(func_name: str, end: exp.Expr) -> exp.Expr:
4193            lst = exp.Bracket(
4194                this=exp.Anonymous(this=func_name, expressions=[this, pattern]),
4195                expressions=[exp.Slice(this=exp.Literal.number(1), expression=end)],
4196                offset=1,
4197            )
4198            transform = exp.Anonymous(
4199                this="LIST_TRANSFORM",
4200                expressions=[
4201                    lst,
4202                    exp.Lambda(
4203                        this=exp.Length(this=exp.to_identifier("x")),
4204                        expressions=[exp.to_identifier("x")],
4205                    ),
4206                ],
4207            )
4208            return exp.Coalesce(
4209                this=exp.Anonymous(this="LIST_SUM", expressions=[transform]),
4210                expressions=[exp.Literal.number(0)],
4211            )
4212
4213        # Position = 1 + sum(split_lengths[1:occ]) + sum(match_lengths[1:occ-1]) + offset
4214        base_pos: exp.Expr = (
4215            exp.Literal.number(1)
4216            + sum_lengths("STRING_SPLIT_REGEX", occurrence)
4217            + sum_lengths("REGEXP_EXTRACT_ALL", occurrence - exp.Literal.number(1))
4218            + pos_offset
4219        )
4220
4221        # option=1: add match length for end position
4222        if option and option.is_int and option.to_py() == 1:
4223            match_at_occ = exp.Bracket(
4224                this=exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern]),
4225                expressions=[occurrence],
4226                offset=1,
4227            )
4228            base_pos = base_pos + exp.Coalesce(
4229                this=exp.Length(this=match_at_occ), expressions=[exp.Literal.number(0)]
4230            )
4231
4232        # NULL checks for all provided arguments
4233        # .copy() is used strictly because .is_() alters the node's parent pointer, mutating the parsed AST
4234        null_args = [
4235            expression.this,
4236            expression.expression,
4237            position,
4238            orig_occ,
4239            option,
4240            parameters,
4241        ]
4242        null_checks = [arg.copy().is_(exp.Null()) for arg in null_args if arg]
4243
4244        matches = exp.Anonymous(this="REGEXP_EXTRACT_ALL", expressions=[this, pattern])
4245
4246        return self.sql(
4247            exp.case()
4248            .when(exp.or_(*null_checks), exp.Null())
4249            .when(pattern.copy().eq(exp.Literal.string("")), exp.Literal.number(0))
4250            .when(exp.Length(this=matches) < occurrence, exp.Literal.number(0))
4251            .else_(base_pos)
4252        )
@unsupported_args('culture')
def numbertostr_sql(self, expression: sqlglot.expressions.string.NumberToStr) -> str:
4254    @unsupported_args("culture")
4255    def numbertostr_sql(self, expression: exp.NumberToStr) -> str:
4256        fmt = expression.args.get("format")
4257        if fmt and fmt.is_int:
4258            return self.func("FORMAT", f"'{{:,.{fmt.name}f}}'", expression.this)
4259
4260        self.unsupported("Only integer formats are supported by NumberToStr")
4261        return self.function_fallback_sql(expression)
def autoincrementcolumnconstraint_sql(self, _) -> str:
4263    def autoincrementcolumnconstraint_sql(self, _) -> str:
4264        self.unsupported("The AUTOINCREMENT column constraint is not supported by DuckDB")
4265        return ""
def aliases_sql(self, expression: sqlglot.expressions.core.Aliases) -> str:
4267    def aliases_sql(self, expression: exp.Aliases) -> str:
4268        this = expression.this
4269        if isinstance(this, exp.Posexplode):
4270            return self.posexplode_sql(this)
4271
4272        return super().aliases_sql(expression)
def posexplode_sql(self, expression: sqlglot.expressions.array.Posexplode) -> str:
4274    def posexplode_sql(self, expression: exp.Posexplode) -> str:
4275        this = expression.this
4276        parent = expression.parent
4277
4278        # The default Spark aliases are "pos" and "col", unless specified otherwise
4279        pos, col = exp.to_identifier("pos"), exp.to_identifier("col")
4280
4281        if isinstance(parent, exp.Aliases):
4282            # Column case: SELECT POSEXPLODE(col) [AS (a, b)]
4283            pos, col = parent.expressions
4284        elif isinstance(parent, exp.Table):
4285            # Table case: SELECT * FROM POSEXPLODE(col) [AS (a, b)]
4286            alias = parent.args.get("alias")
4287            if alias:
4288                pos, col = alias.columns or [pos, col]
4289                alias.pop()
4290
4291        # Translate POSEXPLODE to UNNEST + GENERATE_SUBSCRIPTS
4292        # Note: In Spark pos is 0-indexed, but in DuckDB it's 1-indexed, so we subtract 1 from GENERATE_SUBSCRIPTS
4293        unnest_sql = self.sql(exp.Unnest(expressions=[this], alias=col))
4294        gen_subscripts = self.sql(
4295            exp.Alias(
4296                this=exp.Anonymous(
4297                    this="GENERATE_SUBSCRIPTS", expressions=[this, exp.Literal.number(1)]
4298                )
4299                - exp.Literal.number(1),
4300                alias=pos,
4301            )
4302        )
4303
4304        posexplode_sql = self.format_args(gen_subscripts, unnest_sql)
4305
4306        if isinstance(parent, exp.From) or (parent and isinstance(parent.parent, exp.From)):
4307            # SELECT * FROM POSEXPLODE(col) -> SELECT * FROM (SELECT GENERATE_SUBSCRIPTS(...), UNNEST(...))
4308            return self.sql(exp.Subquery(this=exp.Select(expressions=[posexplode_sql])))
4309
4310        return posexplode_sql
def addmonths_sql(self, expression: sqlglot.expressions.temporal.AddMonths) -> str:
4312    def addmonths_sql(self, expression: exp.AddMonths) -> str:
4313        """
4314        Handles three key issues:
4315        1. Float/decimal months: e.g., Snowflake rounds, whereas DuckDB INTERVAL requires integers
4316        2. End-of-month preservation: If input is last day of month, result is last day of result month
4317        3. Type preservation: Maintains DATE/TIMESTAMPTZ types (DuckDB defaults to TIMESTAMP)
4318        """
4319        from sqlglot.optimizer.annotate_types import annotate_types
4320
4321        this = expression.this
4322        if not this.type:
4323            this = annotate_types(this, dialect=self.dialect)
4324
4325        if this.is_type(*exp.DataType.TEXT_TYPES):
4326            this = exp.Cast(this=this, to=exp.DataType(this=exp.DType.TIMESTAMP))
4327
4328        # Detect float/decimal months to apply rounding (Snowflake behavior)
4329        # DuckDB INTERVAL syntax doesn't support non-integer expressions, so use TO_MONTHS
4330        months_expr = expression.expression
4331        if not months_expr.type:
4332            months_expr = annotate_types(months_expr, dialect=self.dialect)
4333
4334        # Build interval or to_months expression based on type
4335        # Float/decimal case: Round and use TO_MONTHS(CAST(ROUND(value) AS INT))
4336        interval_or_to_months = (
4337            exp.func("TO_MONTHS", exp.cast(exp.func("ROUND", months_expr), "INT"))
4338            if months_expr.is_type(
4339                exp.DType.FLOAT,
4340                exp.DType.DOUBLE,
4341                exp.DType.DECIMAL,
4342            )
4343            # Integer case: standard INTERVAL N MONTH syntax
4344            else exp.Interval(this=months_expr, unit=exp.var("MONTH"))
4345        )
4346
4347        date_add_expr = exp.Add(this=this, expression=interval_or_to_months)
4348
4349        # Apply end-of-month preservation if Snowflake flag is set
4350        # CASE WHEN LAST_DAY(date) = date THEN LAST_DAY(result) ELSE result END
4351        preserve_eom = expression.args.get("preserve_end_of_month")
4352        result_expr = (
4353            exp.case()
4354            .when(
4355                exp.EQ(this=exp.func("LAST_DAY", this), expression=this),
4356                exp.func("LAST_DAY", date_add_expr),
4357            )
4358            .else_(date_add_expr)
4359            if preserve_eom
4360            else date_add_expr
4361        )
4362
4363        # DuckDB's DATE_ADD function returns TIMESTAMP/DATETIME by default, even when the input is DATE
4364        # To match for example Snowflake's ADD_MONTHS behavior (which preserves the input type)
4365        # We need to cast the result back to the original type when the input is DATE or TIMESTAMPTZ
4366        # Example: ADD_MONTHS('2023-01-31'::date, 1) should return DATE, not TIMESTAMP
4367        if this.is_type(exp.DType.DATE, exp.DType.TIMESTAMPTZ):
4368            return self.sql(exp.Cast(this=result_expr, to=this.type))
4369        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:
4371    def format_sql(self, expression: exp.Format) -> str:
4372        if expression.name.lower() == "%s" and len(expression.expressions) == 1:
4373            return self.func("FORMAT", "'{}'", expression.expressions[0])
4374
4375        return self.function_fallback_sql(expression)
def hexstring_sql( self, expression: sqlglot.expressions.query.HexString, binary_function_repr: str | None = None) -> str:
4377    def hexstring_sql(
4378        self, expression: exp.HexString, binary_function_repr: str | None = None
4379    ) -> str:
4380        # UNHEX('FF') correctly produces blob \xFF in DuckDB
4381        return super().hexstring_sql(expression, binary_function_repr="UNHEX")
def datetrunc_sql(self, expression: sqlglot.expressions.temporal.DateTrunc) -> str:
4383    def datetrunc_sql(self, expression: exp.DateTrunc) -> str:
4384        unit = expression.args.get("unit")
4385        date = expression.this
4386
4387        week_start = _week_unit_to_dow(unit)
4388        unit = unit_to_str(expression)
4389
4390        if week_start:
4391            result = self.sql(
4392                _build_week_trunc_expression(date, week_start, preserve_start_day=True)
4393            )
4394        else:
4395            result = self.func("DATE_TRUNC", unit, date)
4396
4397        if (
4398            expression.args.get("input_type_preserved")
4399            and date.is_type(*exp.DataType.TEMPORAL_TYPES)
4400            and not (is_date_unit(unit) and date.is_type(exp.DType.DATE))
4401        ):
4402            return self.sql(exp.Cast(this=result, to=date.type))
4403
4404        return result
def timestamptrunc_sql(self, expression: sqlglot.expressions.temporal.TimestampTrunc) -> str:
4406    def timestamptrunc_sql(self, expression: exp.TimestampTrunc) -> str:
4407        unit = unit_to_str(expression)
4408        zone = expression.args.get("zone")
4409        timestamp = expression.this
4410        date_unit = is_date_unit(unit)
4411
4412        if date_unit and zone:
4413            # BigQuery's TIMESTAMP_TRUNC with timezone truncates in the target timezone and returns as UTC.
4414            # Double AT TIME ZONE needed for BigQuery compatibility:
4415            # 1. First AT TIME ZONE: ensures truncation happens in the target timezone
4416            # 2. Second AT TIME ZONE: converts the DATE result back to TIMESTAMPTZ (preserving time component)
4417            timestamp = exp.AtTimeZone(this=timestamp, zone=zone)
4418            result_sql = self.func("DATE_TRUNC", unit, timestamp)
4419            return self.sql(exp.AtTimeZone(this=result_sql, zone=zone))
4420
4421        result = self.func("DATE_TRUNC", unit, timestamp)
4422        if expression.args.get("input_type_preserved"):
4423            if timestamp.type and timestamp.is_type(exp.DType.TIME, exp.DType.TIMETZ):
4424                dummy_date = exp.Cast(
4425                    this=exp.Literal.string("1970-01-01"),
4426                    to=exp.DataType(this=exp.DType.DATE),
4427                )
4428                date_time = exp.Add(this=dummy_date, expression=timestamp)
4429                result = self.func("DATE_TRUNC", unit, date_time)
4430                return self.sql(exp.Cast(this=result, to=timestamp.type))
4431
4432            if timestamp.is_type(*exp.DataType.TEMPORAL_TYPES) and not (
4433                date_unit and timestamp.is_type(exp.DType.DATE)
4434            ):
4435                return self.sql(exp.Cast(this=result, to=timestamp.type))
4436
4437        return result
def trim_sql(self, expression: sqlglot.expressions.string.Trim) -> str:
4439    def trim_sql(self, expression: exp.Trim) -> str:
4440        expression.this.replace(_cast_to_varchar(expression.this))
4441        if expression.expression:
4442            expression.expression.replace(_cast_to_varchar(expression.expression))
4443
4444        result_sql = super().trim_sql(expression)
4445        return _gen_with_cast_to_blob(self, expression, result_sql)
def round_sql(self, expression: sqlglot.expressions.math.Round) -> str:
4447    def round_sql(self, expression: exp.Round) -> str:
4448        this = expression.this
4449        decimals = expression.args.get("decimals")
4450        truncate = expression.args.get("truncate")
4451
4452        # DuckDB requires the scale (decimals) argument to be an INT
4453        # Some dialects (e.g., Snowflake) allow non-integer scales and cast to an integer internally
4454        if decimals is not None and expression.args.get("casts_non_integer_decimals"):
4455            if not (decimals.is_int or decimals.is_type(*exp.DataType.INTEGER_TYPES)):
4456                decimals = exp.cast(decimals, exp.DType.INT)
4457
4458        func = "ROUND"
4459        if truncate:
4460            # BigQuery uses ROUND_HALF_EVEN; Snowflake uses HALF_TO_EVEN
4461            if truncate.this in ("ROUND_HALF_EVEN", "HALF_TO_EVEN"):
4462                func = "ROUND_EVEN"
4463                truncate = None
4464            # BigQuery uses ROUND_HALF_AWAY_FROM_ZERO; Snowflake uses HALF_AWAY_FROM_ZERO
4465            elif truncate.this in ("ROUND_HALF_AWAY_FROM_ZERO", "HALF_AWAY_FROM_ZERO"):
4466                truncate = None
4467
4468        return self.func(func, this, decimals, truncate)
def trycast_sql(self, expression: sqlglot.expressions.functions.TryCast) -> str:
4470    def trycast_sql(self, expression: exp.TryCast) -> str:
4471        to = expression.to
4472        to_type = to.this
4473        src = expression.this
4474
4475        if (
4476            expression.args.get("null_on_text_overflow")
4477            and to_type in exp.DataType.TEXT_TYPES
4478            and to.expressions
4479        ):
4480            return self.sql(
4481                exp.case()
4482                .when(
4483                    exp.LTE(this=exp.func("LENGTH", src), expression=to.expressions[0].this),
4484                    exp.cast(src, "TEXT"),
4485                )
4486                .else_(exp.Null())
4487            )
4488        elif to_type == exp.DType.DATE and expression.args.get("probe_date_format"):
4489            slash_strptime = exp.cast(
4490                exp.func("TRY_STRPTIME", src, exp.Literal.string(self._TRYCAST_DATE_SLASH_FMT)),
4491                "DATE",
4492            )
4493            mon_strptime = exp.cast(
4494                exp.func("TRY_STRPTIME", src, exp.Literal.string(self._TRYCAST_DATE_MON_FMT)),
4495                "DATE",
4496            )
4497            return self.sql(
4498                exp.case()
4499                .when(exp.func("CONTAINS", src, exp.Literal.string("/")), slash_strptime)
4500                .when(
4501                    exp.RegexpLike(this=src, expression=exp.Literal.string("[A-Za-z]")),
4502                    mon_strptime,
4503                )
4504                .else_(exp.TryCast(this=src, to=to))
4505            )
4506        elif (
4507            isinstance(to_type, exp.Interval)
4508            and (unit := to_type.unit)
4509            and expression.args.get("requires_string")
4510        ):
4511            interval_type = exp.DataType.build("INTERVAL")
4512            if isinstance(unit, exp.IntervalSpan):
4513                self.unsupported(
4514                    "TRY_CAST to INTERVAL with span (e.g. HOUR TO MINUTE) is not supported in DuckDB"
4515                )
4516                return self.sql(exp.TryCast(this=src, to=interval_type))
4517            return self.sql(
4518                exp.TryCast(
4519                    this=exp.DPipe(this=src, expression=exp.Literal.string(f" {unit.name}")),
4520                    to=interval_type,
4521                )
4522            )
4523
4524        return super().trycast_sql(expression)
def strtok_sql(self, expression: sqlglot.expressions.string.Strtok) -> str:
4526    def strtok_sql(self, expression: exp.Strtok) -> str:
4527        string_arg = expression.this
4528        delimiter_arg = expression.args.get("delimiter")
4529        part_index_arg = expression.args.get("part_index")
4530
4531        if delimiter_arg and part_index_arg:
4532            # Escape regex chars and build character class at runtime using REGEXP_REPLACE
4533            escaped_delimiter = exp.Anonymous(
4534                this="REGEXP_REPLACE",
4535                expressions=[
4536                    delimiter_arg,
4537                    exp.Literal.string(
4538                        r"([\[\]^.\-*+?(){}|$\\])"
4539                    ),  # Escape problematic regex chars
4540                    exp.Literal.string(
4541                        r"\\\1"
4542                    ),  # Replace with escaped version using $1 backreference
4543                    exp.Literal.string("g"),  # Global flag
4544                ],
4545            )
4546            # CASE WHEN delimiter = '' THEN '' ELSE CONCAT('[', escaped_delimiter, ']') END
4547            regex_pattern = (
4548                exp.case()
4549                .when(delimiter_arg.eq(exp.Literal.string("")), exp.Literal.string(""))
4550                .else_(
4551                    exp.func(
4552                        "CONCAT",
4553                        exp.Literal.string("["),
4554                        escaped_delimiter,
4555                        exp.Literal.string("]"),
4556                    )
4557                )
4558            )
4559
4560            # STRTOK skips empty strings, so we need to filter them out
4561            # LIST_FILTER(REGEXP_SPLIT_TO_ARRAY(string, pattern), x -> x != '')[index]
4562            split_array = exp.func("REGEXP_SPLIT_TO_ARRAY", string_arg, regex_pattern)
4563            x = exp.to_identifier("x")
4564            is_empty = x.eq(exp.Literal.string(""))
4565            filtered_array = exp.func(
4566                "LIST_FILTER",
4567                split_array,
4568                exp.Lambda(this=exp.not_(is_empty.copy()), expressions=[x.copy()]),
4569            )
4570            base_func = exp.Bracket(
4571                this=filtered_array,
4572                expressions=[part_index_arg],
4573                offset=1,
4574            )
4575
4576            # Use template with the built regex pattern
4577            result = exp.replace_placeholders(
4578                self.STRTOK_TEMPLATE.copy(),
4579                string=string_arg,
4580                delimiter=delimiter_arg,
4581                part_index=part_index_arg,
4582                base_func=base_func,
4583            )
4584
4585            return self.sql(result)
4586
4587        return self.function_fallback_sql(expression)
def strtoktoarray_sql(self, expression: sqlglot.expressions.array.StrtokToArray) -> str:
4589    def strtoktoarray_sql(self, expression: exp.StrtokToArray) -> str:
4590        string_arg = expression.this
4591        delimiter_arg = expression.args.get("expression") or exp.Literal.string(" ")
4592
4593        escaped = exp.RegexpReplace(
4594            this=delimiter_arg.copy(),
4595            expression=exp.Literal.string(r"([\[\]^.\-*+?(){}|$\\])"),
4596            replacement=exp.Literal.string(r"\\\1"),
4597            modifiers=exp.Literal.string("g"),
4598        )
4599        return self.sql(
4600            exp.replace_placeholders(
4601                self.STRTOK_TO_ARRAY_TEMPLATE.copy(),
4602                string=string_arg,
4603                delimiter=delimiter_arg,
4604                escaped=escaped,
4605            )
4606        )
def approxquantile_sql(self, expression: sqlglot.expressions.aggregate.ApproxQuantile) -> str:
4608    def approxquantile_sql(self, expression: exp.ApproxQuantile) -> str:
4609        result = self.func("APPROX_QUANTILE", expression.this, expression.args.get("quantile"))
4610
4611        # DuckDB returns integers for APPROX_QUANTILE, cast to DOUBLE if the expected type is a real type
4612        if expression.is_type(*exp.DataType.REAL_TYPES):
4613            result = f"CAST({result} AS DOUBLE)"
4614
4615        return result
def approxquantiles_sql(self, expression: sqlglot.expressions.aggregate.ApproxQuantiles) -> str:
4617    def approxquantiles_sql(self, expression: exp.ApproxQuantiles) -> str:
4618        """
4619        BigQuery's APPROX_QUANTILES(expr, n) returns an array of n+1 approximate quantile values
4620        dividing the input distribution into n equal-sized buckets.
4621
4622        Both BigQuery and DuckDB use approximate algorithms for quantile estimation, but BigQuery
4623        does not document the specific algorithm used so results may differ. DuckDB does not
4624        support RESPECT NULLS.
4625        """
4626        this = expression.this
4627        if isinstance(this, exp.Distinct):
4628            # APPROX_QUANTILES requires 2 args and DISTINCT node grabs both
4629            if len(this.expressions) < 2:
4630                self.unsupported("APPROX_QUANTILES requires a bucket count argument")
4631                return self.function_fallback_sql(expression)
4632            num_quantiles_expr = this.expressions[1].pop()
4633        else:
4634            num_quantiles_expr = expression.expression
4635
4636        if not isinstance(num_quantiles_expr, exp.Literal) or not num_quantiles_expr.is_int:
4637            self.unsupported("APPROX_QUANTILES bucket count must be a positive integer")
4638            return self.function_fallback_sql(expression)
4639
4640        num_quantiles = t.cast(int, num_quantiles_expr.to_py())
4641        if num_quantiles <= 0:
4642            self.unsupported("APPROX_QUANTILES bucket count must be a positive integer")
4643            return self.function_fallback_sql(expression)
4644
4645        quantiles = [
4646            exp.Literal.number(Decimal(i) / Decimal(num_quantiles))
4647            for i in range(num_quantiles + 1)
4648        ]
4649
4650        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:
4652    def jsonextractscalar_sql(self, expression: exp.JSONExtractScalar) -> str:
4653        if expression.args.get("scalar_only"):
4654            expression = exp.JSONExtractScalar(
4655                this=rename_func("JSON_VALUE")(self, expression), expression="'$'"
4656            )
4657        return _arrow_json_extract_sql(self, expression)
def bitwisenot_sql(self, expression: sqlglot.expressions.core.BitwiseNot) -> str:
4659    def bitwisenot_sql(self, expression: exp.BitwiseNot) -> str:
4660        this = expression.this
4661
4662        if _is_binary(this):
4663            expression.type = exp.DType.BINARY.into_expr()
4664
4665        arg = _cast_to_bit(this)
4666
4667        if isinstance(this, exp.Neg):
4668            arg = exp.Paren(this=arg)
4669
4670        expression.set("this", arg)
4671
4672        result_sql = f"~{self.sql(expression, 'this')}"
4673
4674        return _gen_with_cast_to_blob(self, expression, result_sql)
def window_sql(self, expression: sqlglot.expressions.query.Window) -> str:
4676    def window_sql(self, expression: exp.Window) -> str:
4677        this = expression.this
4678        if isinstance(this, exp.Corr) or (
4679            isinstance(this, exp.Filter) and isinstance(this.this, exp.Corr)
4680        ):
4681            return self._corr_sql(expression)
4682
4683        return super().window_sql(expression)
def filter_sql(self, expression: sqlglot.expressions.core.Filter) -> str:
4685    def filter_sql(self, expression: exp.Filter) -> str:
4686        if isinstance(expression.this, exp.Corr):
4687            return self._corr_sql(expression)
4688
4689        return super().filter_sql(expression)
def uuid_sql(self, expression: sqlglot.expressions.functions.Uuid) -> str:
4708    def uuid_sql(self, expression: exp.Uuid) -> str:
4709        namespace = expression.this
4710        name = expression.args.get("name")
4711
4712        # UUID v5 (namespace + name) - Emulate using SHA1
4713        if namespace and name:
4714            result = exp.replace_placeholders(
4715                self.UUID_V5_TEMPLATE.copy(),
4716                namespace=namespace,
4717                name=name,
4718            )
4719            return self.sql(result)
4720
4721        return super().uuid_sql(expression)
Inherited Members
sqlglot.generator.Generator
Generator
NULL_ORDERING_SUPPORTED
WINDOW_FUNCS_WITH_NULL_ORDERING
LOCKING_READS_SUPPORTED
EXCEPT_INTERSECT_SUPPORT_ALL_CLAUSE
WRAP_DERIVED_VALUES
CREATE_FUNCTION_RETURN_AS
MATCHED_BY_SOURCE
SUPPORTS_MERGE_WHERE
SINGLE_STRING_INTERVAL
INTERVAL_ALLOWS_PLURAL_FORM
LIMIT_ONLY_LITERALS
GROUPINGS_SEP
INDEX_ON
INOUT_SEPARATOR
DIRECTED_JOINS
QUERY_HINT_SEP
IS_BOOL_ALLOWED
DUPLICATE_KEY_UPDATE_WITH_SET
LIMIT_IS_TOP
RETURNING_END
EXTRACT_ALLOWS_QUOTES
TZ_TO_WITH_TIME_ZONE
VALUES_AS_TABLE
ALTER_TABLE_INCLUDE_COLUMN_KEYWORD
UNNEST_WITH_ORDINALITY
AGGREGATE_FILTER_SUPPORTED
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
INSERT_OVERWRITE
SUPPORTS_SELECT_INTO
SUPPORTS_UNLOGGED_TABLES
SUPPORTS_MODIFY_COLUMN
SUPPORTS_CHANGE_COLUMN
LIKE_PROPERTY_INSIDE_SCHEMA
JSON_TYPE_REQUIRED_FOR_EXTRACTION
JSON_PATH_SINGLE_QUOTE_ESCAPE
JSON_PATH_KEY_QUOTED_FORCES_BRACKETS
SET_OP_MODIFIERS
COPY_PARAMS_ARE_WRAPPED
COPY_PARAMS_EQ_REQUIRED
TRY_SUPPORTED
SUPPORTS_UESCAPE
UNICODE_SUBSTITUTE
HEX_FUNC
WITH_PROPERTIES_PREFIX
QUOTE_JSON_PATH
SUPPORTS_EXPLODING_PROJECTIONS
ARRAY_CONCAT_IS_VAR_LEN
SUPPORTS_CONVERT_TIMEZONE
SUPPORTS_MEDIAN
SUPPORTS_UNIX_SECONDS
ALTER_SET_WRAPPED
PARSE_JSON_NAME
ARRAY_SIZE_NAME
ALTER_SET_TYPE
SUPPORTS_BETWEEN_FLAGS
MATCH_AGAINST_TABLE_PREFIX
DECLARE_DEFAULT_ASSIGNMENT
UPDATE_STATEMENT_SUPPORTS_FROM
STAR_EXCLUDE_REQUIRES_DERIVED_TABLE
UNSUPPORTED_TYPES
TIME_PART_SINGULARS
TOKEN_MAPPING
EXPRESSION_PRECEDES_PROPERTIES_CREATABLES
WITH_SEPARATED_COMMENTS
EXCLUDE_COMMENTS
PARAMETERIZABLE_TEXT_TYPES
EXPRESSIONS_WITHOUT_NESTED_CTES
RESPECT_IGNORE_NULLS_UNSUPPORTED_EXPRESSIONS
SAFE_JSON_PATH_KEY_RE
SENTINEL_LINE_BREAK
pretty
identify
normalize
pad
unsupported_level
max_unsupported
leading_comma
max_text_width
comments
dialect
normalize_functions
unsupported_messages
generate
preprocess
unsupported
sep
seg
sanitize_comment
maybe_comment
wrap
no_identify
normalize_func
indent
sql
uncache_sql
cache_sql
characterset_sql
column_parts
column_sql
pseudocolumn_sql
columnposition_sql
columndef_sql
columnconstraint_sql
computedcolumnconstraint_sql
compresscolumnconstraint_sql
generatedasidentitycolumnconstraint_sql
generatedasrowcolumnconstraint_sql
periodforsystemtimeconstraint_sql
notnullcolumnconstraint_sql
primarykeycolumnconstraint_sql
uniquecolumnconstraint_sql
inoutcolumnconstraint_sql
createable_sql
create_sql
sequenceproperties_sql
triggerproperties_sql
triggerreferencing_sql
triggerevent_sql
clone_sql
describe_sql
heredoc_sql
prepend_ctes
with_sql
cte_sql
tablealias_sql
bitstring_sql
bytestring_sql
unicodestring_sql
rawstring_sql
datatypeparam_sql
datatype_param_bound_limiter
datatype_sql
directory_sql
delete_sql
drop_sql
set_operation
set_operations
fetch_sql
limitoptions_sql
hint_sql
indexparameters_sql
index_sql
dynamicidentifier_sql
identifier_sql
lowerhex_sql
inputoutputformat_sql
national_sql
partition_sql
properties_sql
root_properties
properties
with_properties
locate_properties
property_name
property_sql
uuidproperty_sql
likeproperty_sql
fallbackproperty_sql
journalproperty_sql
freespaceproperty_sql
checksumproperty_sql
mergeblockratioproperty_sql
moduleproperty_sql
datablocksizeproperty_sql
blockcompressionproperty_sql
isolatedloadingproperty_sql
partitionboundspec_sql
partitionedofproperty_sql
lockingproperty_sql
withdataproperty_sql
withsystemversioningproperty_sql
insert_sql
introducer_sql
kill_sql
pseudotype_sql
objectidentifier_sql
onconflict_sql
returning_sql
rowformatdelimitedproperty_sql
withtablehint_sql
indextablehint_sql
historicaldata_sql
table_parts
table_sql
pivot_sql
version_sql
tuple_sql
update_sql
values_sql
var_sql
into_sql
from_sql
groupingsets_sql
rollup_sql
rollupindex_sql
rollupproperty_sql
cube_sql
group_sql
having_sql
connect_sql
prior_sql
lateral_op
lateral_sql
limit_sql
offset_sql
setitem_sql
set_sql
queryband_sql
pragma_sql
lock_sql
literal_sql
escape_str
loaddata_sql
null_sql
boolean_sql
booland_sql
boolor_sql
order_sql
withfill_sql
cluster_sql
clusterproperty_sql
distribute_sql
sort_sql
ordered_sql
matchrecognizemeasure_sql
matchrecognize_sql
query_modifiers
options_modifier
forclause_sql
queryoption_sql
offset_limit_modifiers
after_limit_modifiers
select_sql
schema_sql
schema_columns_sql
star_sql
parameter_sql
sessionparameter_sql
placeholder_sql
subquery_sql
qualify_sql
prewhere_sql
where_sql
partition_by_sql
windowspec_sql
between_sql
bracket_offset_expressions
all_sql
any_sql
exists_sql
case_sql
constraint_sql
nextvaluefor_sql
convert_concat_args
concat_sql
check_sql
foreignkey_sql
primarykey_sql
if_sql
matchagainst_sql
jsonkeyvalue_sql
jsonpath_sql
json_path_part
formatjson_sql
formatphrase_sql
jsonarray_sql
jsonarrayagg_sql
jsoncolumndef_sql
jsonschema_sql
jsontable_sql
openjsoncolumndef_sql
openjson_sql
in_sql
in_unnest_op
interval_sql
return_sql
reference_sql
anonymous_sql
paren_sql
neg_sql
not_sql
alias_sql
pivotalias_sql
atindex_sql
attimezone_sql
fromtimezone_sql
fromiso8601date_sql
fromiso8601timestamp_sql
add_sql
and_sql
or_sql
xor_sql
connector_sql
bitwiseand_sql
bitwiseleftshift_sql
bitwiseor_sql
bitwiserightshift_sql
cast_sql
command_sql
comment_sql
mergetreettlaction_sql
mergetreettl_sql
transaction_sql
commit_sql
rollback_sql
altercolumn_sql
modifycolumn_sql
alterindex_sql
alterdiststyle_sql
altersortkey_sql
alterrename_sql
renamecolumn_sql
alterset_sql
alter_sql
altersession_sql
add_column_sql
droppartition_sql
dropprimarykey_sql
addconstraint_sql
addpartition_sql
distinct_sql
havingmax_sql
intdiv_sql
dpipe_sql
div_sql
safedivide_sql
overlaps_sql
distance_sql
distancend_sql
dot_sql
eq_sql
propertyeq_sql
escape_sql
glob_sql
gt_sql
gte_sql
is_sql
like_sql
ilike_sql
match_sql
similarto_sql
lt_sql
lte_sql
mod_sql
mul_sql
neq_sql
nullsafeeq_sql
nullsafeneq_sql
sub_sql
jsoncast_sql
try_sql
log_sql
use_sql
binary
ceil_floor
function_fallback_sql
func
format_args
too_wide
format_time
expressions
op_expressions
naked_property
tag_sql
token_sql
userdefinedfunction_sql
macrooverloads_sql
macrooverload_sql
joinhint_sql
kwarg_sql
when_sql
whens_sql
merge_sql
tochar_sql
dictproperty_sql
dictrange_sql
dictsubproperty_sql
duplicatekeyproperty_sql
uniquekeyproperty_sql
distributedbyproperty_sql
oncluster_sql
clusteredbyproperty_sql
anyvalue_sql
querytransform_sql
indexconstraintoption_sql
checkcolumnconstraint_sql
indexcolumnconstraint_sql
nvl2_sql
comprehension_sql
columnprefix_sql
opclass_sql
predict_sql
generateembedding_sql
generatetext_sql
generatetable_sql
generatebool_sql
generateint_sql
generatedouble_sql
mltranslate_sql
mlforecast_sql
aiforecast_sql
featuresattime_sql
vectorsearch_sql
forin_sql
refresh_sql
toarray_sql
tsordstotimestamp_sql
tsordstodatetime_sql
tsordstodate_sql
unixdate_sql
lastday_sql
dateadd_sql
arrayany_sql
struct_sql
partitionrange_sql
truncatetable_sql
convert_sql
copyparameter_sql
credentials_sql
copy_sql
semicolon_sql
datadeletionproperty_sql
maskingpolicycolumnconstraint_sql
gapfill_sql
scope_resolution
scoperesolution_sql
changes_sql
summarize_sql
explodinggenerateseries_sql
converttimezone_sql
json_sql
jsonvalue_sql
skipjsoncolumn_sql
conditionalinsert_sql
multitableinserts_sql
oncondition_sql
jsonextractquote_sql
jsonexists_sql
arrayagg_sql
slice_sql
apply_sql
grant_sql
revoke_sql
grantprivilege_sql
grantprincipal_sql
columns_sql
overlay_sql
todouble_sql
string_sql
median_sql
overflowtruncatebehavior_sql
unixseconds_sql
arraysize_sql
attach_sql
detach_sql
attachoption_sql
watermarkcolumnconstraint_sql
encodeproperty_sql
includeproperty_sql
xmlelement_sql
xmlkeyvalueoption_sql
partitionbyrangeproperty_sql
partitionbyrangepropertydynamic_sql
unpivotcolumns_sql
analyzesample_sql
analyzestatistics_sql
analyzehistogram_sql
analyzedelete_sql
analyzelistchainedrows_sql
analyzevalidate_sql
analyze_sql
xmltable_sql
xmlnamespace_sql
export_sql
declare_sql
declareitem_sql
recursivewithsearch_sql
parameterizedagg_sql
anonymousaggfunc_sql
combinedaggfunc_sql
combinedparameterizedagg_sql
get_put_sql
translatecharacters_sql
decodecase_sql
semanticview_sql
getextract_sql
datefromunixdate_sql
buildproperty_sql
refreshtriggerproperty_sql
modelattribute_sql
directorystage_sql
initcap_sql
localtime_sql
localtimestamp_sql
weekstart_sql
block_sql
storedprocedure_sql
ifblock_sql
whileblock_sql
execute_sql
executesql_sql
altermodifysqlsecurity_sql
usingproperty_sql
renameindex_sql