Edit on GitHub

sqlglot.parser

    1from __future__ import annotations
    2
    3import itertools
    4import logging
    5import re
    6import typing as t
    7from builtins import type as Type
    8from collections import defaultdict
    9from collections.abc import Sequence
   10
   11from sqlglot import exp
   12from sqlglot._typing import F
   13from sqlglot.errors import (
   14    ErrorLevel,
   15    ParseError,
   16    TokenError,
   17    concat_messages,
   18    highlight_sql,
   19    merge_errors,
   20)
   21from sqlglot.expressions import apply_index_offset
   22from sqlglot.helper import ensure_list, i64, seq_get
   23from sqlglot.optimizer.scope import find_in_scope
   24from sqlglot.time import format_time
   25from sqlglot.tokens import Token, Tokenizer, TokenType
   26from sqlglot.trie import TrieResult, in_trie, new_trie
   27
   28if t.TYPE_CHECKING:
   29    from re import Pattern
   30
   31    from sqlglot._typing import BuilderArgs, E
   32    from sqlglot.dialects.dialect import Dialect, DialectType
   33    from sqlglot.expressions import ExpOrStr
   34
   35    T = t.TypeVar("T")
   36    TCeilFloor = t.TypeVar("TCeilFloor", exp.Ceil, exp.Floor)
   37
   38logger = logging.getLogger("sqlglot")
   39
   40OPTIONS_TYPE = dict[str, Sequence[t.Union[Sequence[str], str]]]
   41
   42# Excludes bare strings, which are also collections of strings, so that a single keyword
   43# can't accidentally be matched with substring semantics (e.g. _match_texts("FOO"))
   44TEXTS_TYPE = t.Union[tuple[str, ...], list[str], t.AbstractSet[str], t.Mapping[str, t.Any]]
   45
   46# Used to detect alphabetical characters and +/- in timestamp literals
   47TIME_ZONE_RE: Pattern[str] = re.compile(r":.*?[a-zA-Z\+\-]")
   48
   49
   50def build_var_map(args: BuilderArgs) -> exp.StarMap | exp.VarMap:
   51    if len(args) == 1 and args[0].is_star:
   52        return exp.StarMap(this=args[0])
   53
   54    keys: list[ExpOrStr] = []
   55    values: list[ExpOrStr] = []
   56    for i in range(0, len(args), 2):
   57        keys.append(args[i])
   58        values.append(args[i + 1])
   59
   60    return exp.VarMap(keys=exp.array(*keys, copy=False), values=exp.array(*values, copy=False))
   61
   62
   63def build_like(args: BuilderArgs) -> exp.Escape | exp.Like:
   64    like = exp.Like(this=seq_get(args, 1), expression=seq_get(args, 0))
   65    return exp.Escape(this=like, expression=seq_get(args, 2)) if len(args) > 2 else like
   66
   67
   68def binary_range_parser(
   69    expr_type: Type[exp.Expr], reverse_args: bool = False
   70) -> t.Callable[[Parser, exp.Expr | None], exp.Expr | None]:
   71    def _parse_binary_range(self: Parser, this: exp.Expr | None) -> exp.Expr | None:
   72        expression = self._parse_bitwise()
   73        if reverse_args:
   74            this, expression = expression, this
   75        return self._parse_escape(self.expression(expr_type(this=this, expression=expression)))
   76
   77    return _parse_binary_range
   78
   79
   80def build_logarithm(args: BuilderArgs, dialect: Dialect) -> exp.Func:
   81    # Default argument order is base, expression
   82    this = seq_get(args, 0)
   83    expression = seq_get(args, 1)
   84
   85    if expression:
   86        if not dialect.LOG_BASE_FIRST:
   87            this, expression = expression, this
   88        return exp.Log(this=this, expression=expression)
   89
   90    return (exp.Ln if dialect.parser_class.LOG_DEFAULTS_TO_LN else exp.Log)(this=this)
   91
   92
   93def build_hex(args: BuilderArgs, dialect: Dialect) -> exp.Hex | exp.LowerHex:
   94    arg = seq_get(args, 0)
   95    return exp.LowerHex(this=arg) if dialect.HEX_LOWERCASE else exp.Hex(this=arg)
   96
   97
   98def build_lower(args: BuilderArgs) -> exp.Lower | exp.Hex:
   99    # LOWER(HEX(..)) can be simplified to LowerHex to simplify its transpilation
  100    arg = seq_get(args, 0)
  101    return exp.LowerHex(this=arg.this) if isinstance(arg, exp.Hex) else exp.Lower(this=arg)
  102
  103
  104def build_upper(args: BuilderArgs) -> exp.Upper | exp.Hex:
  105    # UPPER(HEX(..)) can be simplified to Hex to simplify its transpilation
  106    arg = seq_get(args, 0)
  107    return exp.Hex(this=arg.this) if isinstance(arg, exp.Hex) else exp.Upper(this=arg)
  108
  109
  110def build_extract_json_with_path(
  111    expr_type: Type[E],
  112) -> t.Callable[[BuilderArgs, Dialect], E]:
  113    def _builder(args: BuilderArgs, dialect: Dialect) -> E:
  114        expression = expr_type(
  115            this=seq_get(args, 0), expression=dialect.to_json_path(seq_get(args, 1))
  116        )
  117        if len(args) > 2 and expr_type is exp.JSONExtract:
  118            expression.set("expressions", args[2:])
  119        if expr_type is exp.JSONExtractScalar:
  120            expression.set("scalar_only", dialect.JSON_EXTRACT_SCALAR_SCALAR_ONLY)
  121
  122        return expression
  123
  124    return _builder
  125
  126
  127def build_mod(args: BuilderArgs) -> exp.Mod:
  128    this = seq_get(args, 0)
  129    expression = seq_get(args, 1)
  130
  131    # Wrap the operands if they are binary nodes, e.g. MOD(a + 1, 7) -> (a + 1) % 7
  132    this = exp.Paren(this=this) if isinstance(this, exp.Binary) else this
  133    expression = exp.Paren(this=expression) if isinstance(expression, exp.Binary) else expression
  134
  135    return exp.Mod(this=this, expression=expression)
  136
  137
  138def build_pad(args: BuilderArgs, is_left: bool = True):
  139    return exp.Pad(
  140        this=seq_get(args, 0),
  141        expression=seq_get(args, 1),
  142        fill_pattern=seq_get(args, 2),
  143        is_left=is_left,
  144    )
  145
  146
  147def build_array_constructor(
  148    exp_class: Type[E], args: list[t.Any], bracket_kind: TokenType, dialect: Dialect
  149) -> exp.Expr:
  150    array_exp = exp_class(expressions=args)
  151
  152    if exp_class == exp.Array:
  153        array_exp.set("value_constructor", len(args) == 1 and isinstance(args[0], exp.Query))
  154        if dialect.HAS_DISTINCT_ARRAY_CONSTRUCTORS:
  155            array_exp.set("bracket_notation", bracket_kind == TokenType.L_BRACKET)
  156
  157    return array_exp
  158
  159
  160def build_convert_timezone(
  161    args: BuilderArgs, default_source_tz: str | None = None
  162) -> exp.ConvertTimezone | exp.Anonymous:
  163    if len(args) == 2:
  164        source_tz = exp.Literal.string(default_source_tz) if default_source_tz else None
  165        return exp.ConvertTimezone(
  166            source_tz=source_tz, target_tz=seq_get(args, 0), timestamp=seq_get(args, 1)
  167        )
  168
  169    return exp.ConvertTimezone.from_arg_list(args)
  170
  171
  172def build_trim(args: BuilderArgs, is_left: bool = True, reverse_args: bool = False) -> exp.Trim:
  173    this, expression = seq_get(args, 0), seq_get(args, 1)
  174
  175    if expression and reverse_args:
  176        this, expression = expression, this
  177
  178    return exp.Trim(this=this, expression=expression, position="LEADING" if is_left else "TRAILING")
  179
  180
  181def build_coalesce(
  182    args: BuilderArgs, is_nvl: bool | None = None, is_null: bool | None = None
  183) -> exp.Coalesce:
  184    return exp.Coalesce(this=seq_get(args, 0), expressions=args[1:], is_nvl=is_nvl, is_null=is_null)
  185
  186
  187def build_locate_strposition(args: BuilderArgs) -> exp.StrPosition:
  188    return exp.StrPosition(
  189        this=seq_get(args, 1),
  190        substr=seq_get(args, 0),
  191        position=seq_get(args, 2),
  192    )
  193
  194
  195def build_array_append(args: BuilderArgs, dialect: Dialect) -> exp.ArrayAppend:
  196    """
  197    Builds ArrayAppend with NULL propagation semantics based on the dialect configuration.
  198
  199    Some dialects (Databricks, Spark, Snowflake) return NULL when the input array is NULL.
  200    Others (DuckDB, PostgreSQL) create a new single-element array instead.
  201
  202    Args:
  203        args: Function arguments [array, element]
  204        dialect: The dialect to read ARRAY_FUNCS_PROPAGATES_NULLS from
  205
  206    Returns:
  207        ArrayAppend expression with appropriate null_propagation flag
  208    """
  209    return exp.ArrayAppend(
  210        this=seq_get(args, 0),
  211        expression=seq_get(args, 1),
  212        null_propagation=dialect.ARRAY_FUNCS_PROPAGATES_NULLS,
  213    )
  214
  215
  216def build_array_prepend(args: BuilderArgs, dialect: Dialect) -> exp.ArrayPrepend:
  217    """
  218    Builds ArrayPrepend with NULL propagation semantics based on the dialect configuration.
  219
  220    Some dialects (Databricks, Spark, Snowflake) return NULL when the input array is NULL.
  221    Others (DuckDB, PostgreSQL) create a new single-element array instead.
  222
  223    Args:
  224        args: Function arguments [array, element]
  225        dialect: The dialect to read ARRAY_FUNCS_PROPAGATES_NULLS from
  226
  227    Returns:
  228        ArrayPrepend expression with appropriate null_propagation flag
  229    """
  230    return exp.ArrayPrepend(
  231        this=seq_get(args, 0),
  232        expression=seq_get(args, 1),
  233        null_propagation=dialect.ARRAY_FUNCS_PROPAGATES_NULLS,
  234    )
  235
  236
  237def build_array_concat(args: BuilderArgs, dialect: Dialect) -> exp.ArrayConcat:
  238    """
  239    Builds ArrayConcat with NULL propagation semantics based on the dialect configuration.
  240
  241    Some dialects (Redshift, Snowflake) return NULL when any input array is NULL.
  242    Others (DuckDB, PostgreSQL) skip NULL arrays and continue concatenation.
  243
  244    Args:
  245        args: Function arguments [array1, array2, ...] (variadic)
  246        dialect: The dialect to read ARRAY_FUNCS_PROPAGATES_NULLS from
  247
  248    Returns:
  249        ArrayConcat expression with appropriate null_propagation flag
  250    """
  251    return exp.ArrayConcat(
  252        this=seq_get(args, 0),
  253        expressions=args[1:],
  254        null_propagation=dialect.ARRAY_FUNCS_PROPAGATES_NULLS,
  255    )
  256
  257
  258def build_array_remove(args: BuilderArgs, dialect: Dialect) -> exp.ArrayRemove:
  259    """
  260    Builds ArrayRemove with NULL propagation semantics based on the dialect configuration.
  261
  262    Some dialects (Snowflake) return NULL when the removal value is NULL.
  263    Others (DuckDB) may return empty array due to NULL comparison semantics.
  264
  265    Args:
  266        args: Function arguments [array, value_to_remove]
  267        dialect: The dialect to read ARRAY_FUNCS_PROPAGATES_NULLS from
  268
  269    Returns:
  270        ArrayRemove expression with appropriate null_propagation flag
  271    """
  272    return exp.ArrayRemove(
  273        this=seq_get(args, 0),
  274        expression=seq_get(args, 1),
  275        null_propagation=dialect.ARRAY_FUNCS_PROPAGATES_NULLS,
  276    )
  277
  278
  279def _resolve_dialect(dialect: DialectType) -> Dialect:
  280    from sqlglot.dialects.dialect import Dialect
  281
  282    return Dialect.get_or_raise(dialect)
  283
  284
  285def _unpivot_target(expr: exp.Expr) -> exp.Expr:
  286    # UNPIVOT's pre-FOR values and FOR field are new output names, not column references.
  287    if isinstance(expr, exp.Column) and not expr.table:
  288        return expr.this
  289    if isinstance(expr, exp.Tuple):
  290        expr.set("expressions", [_unpivot_target(e) for e in expr.expressions])
  291    return expr
  292
  293
  294# Builders for the JSON `->` / `->>` / `#>` / `#>>` / `?` operators, shared between
  295# COLUMN_OPERATORS (accessor-tier dialects) and JSON_OPERATORS (Postgres/DuckDB's
  296# binary-operator tier).
  297def build_json_extract(self: Parser, this: exp.Expr, path: exp.Expr) -> exp.JSONExtract:
  298    return self.expression(
  299        exp.JSONExtract(
  300            this=this,
  301            expression=self.dialect.to_json_path(path),
  302            only_json_types=self.JSON_ARROWS_REQUIRE_JSON_TYPE,
  303        )
  304    )
  305
  306
  307def build_json_extract_scalar(
  308    self: Parser, this: exp.Expr, path: exp.Expr
  309) -> exp.JSONExtractScalar:
  310    return self.expression(
  311        exp.JSONExtractScalar(
  312            this=this,
  313            expression=self.dialect.to_json_path(path),
  314            only_json_types=self.JSON_ARROWS_REQUIRE_JSON_TYPE,
  315            scalar_only=self.dialect.JSON_EXTRACT_SCALAR_SCALAR_ONLY,
  316        )
  317    )
  318
  319
  320def build_jsonb_extract(self: Parser, this: exp.Expr, path: exp.Expr) -> exp.JSONBExtract:
  321    return self.expression(exp.JSONBExtract(this=this, expression=path))
  322
  323
  324def build_jsonb_extract_scalar(
  325    self: Parser, this: exp.Expr, path: exp.Expr
  326) -> exp.JSONBExtractScalar:
  327    return self.expression(exp.JSONBExtractScalar(this=this, expression=path))
  328
  329
  330def build_jsonb_contains_top_key(
  331    self: Parser, this: exp.Expr, key: exp.Expr
  332) -> exp.JSONBContainsTopKey:
  333    return self.expression(exp.JSONBContainsTopKey(this=this, expression=key))
  334
  335
  336SENTINEL_NONE: Token = Token(TokenType.SENTINEL, "SENTINEL")
  337
  338
  339class Parser:
  340    """
  341    Parser consumes a list of tokens produced by the Tokenizer and produces a parsed syntax tree.
  342
  343    Args:
  344        error_level: The desired error level.
  345            Default: ErrorLevel.IMMEDIATE
  346        error_message_context: The amount of context to capture from a query string when displaying
  347            the error message (in number of characters).
  348            Default: 100
  349        max_errors: Maximum number of error messages to include in a raised ParseError.
  350            This is only relevant if error_level is ErrorLevel.RAISE.
  351            Default: 3
  352        max_nodes: Maximum number of AST nodes to prevent memory exhaustion.
  353            Set to -1 (default) to disable the check.
  354    """
  355
  356    __slots__ = (
  357        "error_level",
  358        "error_message_context",
  359        "max_errors",
  360        "max_nodes",
  361        "dialect",
  362        "sql",
  363        "errors",
  364        "_tokens",
  365        "_index",
  366        "_curr",
  367        "_next",
  368        "_prev",
  369        "_prev_comments",
  370        "_pipe_cte_counter",
  371        "_chunks",
  372        "_chunk_index",
  373        "_tokens_size",
  374        "_node_count",
  375    )
  376
  377    FUNCTIONS: t.ClassVar[dict[str, t.Callable]] = {
  378        **{name: func.from_arg_list for name, func in exp.FUNCTION_BY_NAME.items()},
  379        **dict.fromkeys(("COALESCE", "IFNULL", "NVL"), build_coalesce),
  380        "ARRAY": lambda args, dialect: exp.Array(expressions=args),
  381        "ARRAYAGG": lambda args, dialect: exp.ArrayAgg(
  382            this=seq_get(args, 0), nulls_excluded=dialect.ARRAY_AGG_INCLUDES_NULLS is None or None
  383        ),
  384        "ARRAY_AGG": lambda args, dialect: exp.ArrayAgg(
  385            this=seq_get(args, 0), nulls_excluded=dialect.ARRAY_AGG_INCLUDES_NULLS is None or None
  386        ),
  387        "ARRAY_APPEND": build_array_append,
  388        "ARRAY_CAT": build_array_concat,
  389        "ARRAY_CONCAT": build_array_concat,
  390        "ARRAY_INTERSECT": lambda args: exp.ArrayIntersect(expressions=args),
  391        "ARRAY_INTERSECTION": lambda args: exp.ArrayIntersect(expressions=args),
  392        "ARRAY_PREPEND": build_array_prepend,
  393        "ARRAY_REMOVE": build_array_remove,
  394        "COUNT": lambda args: exp.Count(this=seq_get(args, 0), expressions=args[1:], big_int=True),
  395        "CONCAT": lambda args, dialect: exp.Concat(
  396            expressions=args,
  397            safe=not dialect.STRICT_STRING_CONCAT,
  398            coalesce=dialect.CONCAT_COALESCE,
  399        ),
  400        "CONCAT_WS": lambda args, dialect: exp.ConcatWs(
  401            expressions=args,
  402            safe=not dialect.STRICT_STRING_CONCAT,
  403            coalesce=dialect.CONCAT_WS_COALESCE,
  404        ),
  405        "CONVERT_TIMEZONE": build_convert_timezone,
  406        "DATE_TO_DATE_STR": lambda args: exp.Cast(
  407            this=seq_get(args, 0),
  408            to=exp.DataType(this=exp.DType.TEXT),
  409        ),
  410        "GENERATE_DATE_ARRAY": lambda args: exp.GenerateDateArray(
  411            start=seq_get(args, 0),
  412            end=seq_get(args, 1),
  413            step=seq_get(args, 2) or exp.Interval(this=exp.Literal.string(1), unit=exp.var("DAY")),
  414        ),
  415        "GENERATE_UUID": lambda args, dialect: exp.Uuid(
  416            is_string=dialect.UUID_IS_STRING_TYPE or None
  417        ),
  418        "GLOB": lambda args: exp.Glob(this=seq_get(args, 1), expression=seq_get(args, 0)),
  419        "GREATEST": lambda args, dialect: exp.Greatest(
  420            this=seq_get(args, 0),
  421            expressions=args[1:],
  422            ignore_nulls=dialect.LEAST_GREATEST_IGNORES_NULLS,
  423        ),
  424        "LEAST": lambda args, dialect: exp.Least(
  425            this=seq_get(args, 0),
  426            expressions=args[1:],
  427            ignore_nulls=dialect.LEAST_GREATEST_IGNORES_NULLS,
  428        ),
  429        "HEX": build_hex,
  430        "JSON_EXTRACT": build_extract_json_with_path(exp.JSONExtract),
  431        "JSON_EXTRACT_SCALAR": build_extract_json_with_path(exp.JSONExtractScalar),
  432        "JSON_EXTRACT_PATH_TEXT": build_extract_json_with_path(exp.JSONExtractScalar),
  433        "JSON_KEYS": lambda args, dialect: exp.JSONKeys(
  434            this=seq_get(args, 0), expression=dialect.to_json_path(seq_get(args, 1))
  435        ),
  436        "LIKE": build_like,
  437        "LOG": build_logarithm,
  438        "LOG2": lambda args: exp.Log(this=exp.Literal.number(2), expression=seq_get(args, 0)),
  439        "LOG10": lambda args: exp.Log(this=exp.Literal.number(10), expression=seq_get(args, 0)),
  440        "LOWER": build_lower,
  441        "LPAD": lambda args: build_pad(args),
  442        "LEFTPAD": lambda args: build_pad(args),
  443        "LTRIM": lambda args: build_trim(args),
  444        "MOD": build_mod,
  445        "RIGHTPAD": lambda args: build_pad(args, is_left=False),
  446        "RPAD": lambda args: build_pad(args, is_left=False),
  447        "RTRIM": lambda args: build_trim(args, is_left=False),
  448        "SCOPE_RESOLUTION": lambda args: (
  449            exp.ScopeResolution(expression=seq_get(args, 0))
  450            if len(args) != 2
  451            else exp.ScopeResolution(this=seq_get(args, 0), expression=seq_get(args, 1))
  452        ),
  453        "STRPOS": exp.StrPosition.from_arg_list,
  454        "CHARINDEX": lambda args: build_locate_strposition(args),
  455        "INSTR": exp.StrPosition.from_arg_list,
  456        "LOCATE": lambda args: build_locate_strposition(args),
  457        "TIME_TO_TIME_STR": lambda args: exp.Cast(
  458            this=seq_get(args, 0),
  459            to=exp.DataType(this=exp.DType.TEXT),
  460        ),
  461        "TO_HEX": build_hex,
  462        "TS_OR_DS_TO_DATE_STR": lambda args: exp.Substring(
  463            this=exp.Cast(
  464                this=seq_get(args, 0),
  465                to=exp.DataType(this=exp.DType.TEXT),
  466            ),
  467            start=exp.Literal.number(1),
  468            length=exp.Literal.number(10),
  469        ),
  470        "UNNEST": lambda args: exp.Unnest(expressions=ensure_list(seq_get(args, 0))),
  471        "UPPER": build_upper,
  472        "UUID": lambda args, dialect: exp.Uuid(is_string=dialect.UUID_IS_STRING_TYPE or None),
  473        "UUID_STRING": lambda args, dialect: exp.Uuid(
  474            this=seq_get(args, 0),
  475            name=seq_get(args, 1),
  476            is_string=dialect.UUID_IS_STRING_TYPE or None,
  477        ),
  478        "VAR_MAP": build_var_map,
  479    }
  480
  481    NO_PAREN_FUNCTIONS: t.ClassVar[dict] = {
  482        TokenType.CURRENT_DATE: exp.CurrentDate,
  483        TokenType.CURRENT_DATETIME: exp.CurrentDate,
  484        TokenType.CURRENT_TIME: exp.CurrentTime,
  485        TokenType.CURRENT_TIMESTAMP: exp.CurrentTimestamp,
  486        TokenType.CURRENT_USER: exp.CurrentUser,
  487        TokenType.CURRENT_ROLE: exp.CurrentRole,
  488    }
  489
  490    STRUCT_TYPE_TOKENS: t.ClassVar = {
  491        TokenType.NESTED,
  492        TokenType.OBJECT,
  493        TokenType.STRUCT,
  494        TokenType.UNION,
  495    }
  496
  497    NESTED_TYPE_TOKENS: t.ClassVar = {
  498        TokenType.ARRAY,
  499        TokenType.LIST,
  500        TokenType.LOWCARDINALITY,
  501        TokenType.MAP,
  502        TokenType.NULLABLE,
  503        TokenType.RANGE,
  504        *STRUCT_TYPE_TOKENS,
  505    }
  506
  507    ENUM_TYPE_TOKENS: t.ClassVar = {
  508        TokenType.DYNAMIC,
  509        TokenType.ENUM,
  510        TokenType.ENUM8,
  511        TokenType.ENUM16,
  512    }
  513
  514    AGGREGATE_TYPE_TOKENS: t.ClassVar = {
  515        TokenType.AGGREGATEFUNCTION,
  516        TokenType.SIMPLEAGGREGATEFUNCTION,
  517    }
  518
  519    TYPE_TOKENS: t.ClassVar = {
  520        TokenType.BIT,
  521        TokenType.BOOLEAN,
  522        TokenType.TINYINT,
  523        TokenType.UTINYINT,
  524        TokenType.SMALLINT,
  525        TokenType.USMALLINT,
  526        TokenType.INT,
  527        TokenType.UINT,
  528        TokenType.BIGINT,
  529        TokenType.UBIGINT,
  530        TokenType.BIGNUM,
  531        TokenType.INT128,
  532        TokenType.UINT128,
  533        TokenType.INT256,
  534        TokenType.UINT256,
  535        TokenType.MEDIUMINT,
  536        TokenType.UMEDIUMINT,
  537        TokenType.FIXEDSTRING,
  538        TokenType.FLOAT,
  539        TokenType.DOUBLE,
  540        TokenType.UDOUBLE,
  541        TokenType.CHAR,
  542        TokenType.NCHAR,
  543        TokenType.VARCHAR,
  544        TokenType.NVARCHAR,
  545        TokenType.BPCHAR,
  546        TokenType.TEXT,
  547        TokenType.MEDIUMTEXT,
  548        TokenType.LONGTEXT,
  549        TokenType.BLOB,
  550        TokenType.MEDIUMBLOB,
  551        TokenType.LONGBLOB,
  552        TokenType.BINARY,
  553        TokenType.VARBINARY,
  554        TokenType.JSON,
  555        TokenType.JSONB,
  556        TokenType.INTERVAL,
  557        TokenType.TINYBLOB,
  558        TokenType.TINYTEXT,
  559        TokenType.TIME,
  560        TokenType.TIMETZ,
  561        TokenType.TIME_NS,
  562        TokenType.TIMESTAMP,
  563        TokenType.TIMESTAMP_S,
  564        TokenType.TIMESTAMP_MS,
  565        TokenType.TIMESTAMP_NS,
  566        TokenType.TIMESTAMPTZ,
  567        TokenType.TIMESTAMPLTZ,
  568        TokenType.TIMESTAMPNTZ,
  569        TokenType.DATETIME,
  570        TokenType.DATETIME2,
  571        TokenType.DATETIME64,
  572        TokenType.SMALLDATETIME,
  573        TokenType.DATE,
  574        TokenType.DATE32,
  575        TokenType.INT4RANGE,
  576        TokenType.INT4MULTIRANGE,
  577        TokenType.INT8RANGE,
  578        TokenType.INT8MULTIRANGE,
  579        TokenType.NUMRANGE,
  580        TokenType.NUMMULTIRANGE,
  581        TokenType.TSRANGE,
  582        TokenType.TSMULTIRANGE,
  583        TokenType.TSTZRANGE,
  584        TokenType.TSTZMULTIRANGE,
  585        TokenType.DATERANGE,
  586        TokenType.DATEMULTIRANGE,
  587        TokenType.DECIMAL,
  588        TokenType.DECIMAL32,
  589        TokenType.DECIMAL64,
  590        TokenType.DECIMAL128,
  591        TokenType.DECIMAL256,
  592        TokenType.DECFLOAT,
  593        TokenType.UDECIMAL,
  594        TokenType.BIGDECIMAL,
  595        TokenType.UUID,
  596        TokenType.GEOGRAPHY,
  597        TokenType.GEOGRAPHYPOINT,
  598        TokenType.GEOMETRY,
  599        TokenType.POINT,
  600        TokenType.RING,
  601        TokenType.LINESTRING,
  602        TokenType.MULTILINESTRING,
  603        TokenType.POLYGON,
  604        TokenType.MULTIPOLYGON,
  605        TokenType.HLLSKETCH,
  606        TokenType.HSTORE,
  607        TokenType.PSEUDO_TYPE,
  608        TokenType.SUPER,
  609        TokenType.SERIAL,
  610        TokenType.SMALLSERIAL,
  611        TokenType.BIGSERIAL,
  612        TokenType.XML,
  613        TokenType.YEAR,
  614        TokenType.USERDEFINED,
  615        TokenType.MONEY,
  616        TokenType.SMALLMONEY,
  617        TokenType.ROWVERSION,
  618        TokenType.IMAGE,
  619        TokenType.VARIANT,
  620        TokenType.VECTOR,
  621        TokenType.VOID,
  622        TokenType.OBJECT,
  623        TokenType.OBJECT_IDENTIFIER,
  624        TokenType.INET,
  625        TokenType.IPADDRESS,
  626        TokenType.IPPREFIX,
  627        TokenType.IPV4,
  628        TokenType.IPV6,
  629        TokenType.UNKNOWN,
  630        TokenType.NOTHING,
  631        TokenType.NULL,
  632        TokenType.NAME,
  633        TokenType.TDIGEST,
  634        TokenType.DYNAMIC,
  635        *ENUM_TYPE_TOKENS,
  636        *NESTED_TYPE_TOKENS,
  637        *AGGREGATE_TYPE_TOKENS,
  638    }
  639
  640    SIGNED_TO_UNSIGNED_TYPE_TOKEN: t.ClassVar = {
  641        TokenType.BIGINT: TokenType.UBIGINT,
  642        TokenType.INT: TokenType.UINT,
  643        TokenType.MEDIUMINT: TokenType.UMEDIUMINT,
  644        TokenType.SMALLINT: TokenType.USMALLINT,
  645        TokenType.TINYINT: TokenType.UTINYINT,
  646        TokenType.DECIMAL: TokenType.UDECIMAL,
  647        TokenType.DOUBLE: TokenType.UDOUBLE,
  648    }
  649
  650    SUBQUERY_PREDICATES: t.ClassVar = {
  651        TokenType.ANY: exp.Any,
  652        TokenType.ALL: exp.All,
  653        TokenType.EXISTS: exp.Exists,
  654        TokenType.SOME: exp.Any,
  655    }
  656
  657    SUBQUERY_TOKENS: t.ClassVar = {
  658        TokenType.SELECT,
  659        TokenType.WITH,
  660        TokenType.FROM,
  661    }
  662
  663    RESERVED_TOKENS: t.ClassVar = {
  664        *Tokenizer.SINGLE_TOKENS.values(),
  665        TokenType.SELECT,
  666    } - {TokenType.IDENTIFIER}
  667
  668    # Tokens whose text is extracted from delimited source text (e.g. quoted identifiers,
  669    # string literals), so they must never be treated as keywords when matching by text
  670    TEXT_MATCH_EXCLUDED_TOKENS: t.ClassVar[frozenset] = frozenset(
  671        {
  672            TokenType.BIT_STRING,
  673            TokenType.BYTE_STRING,
  674            TokenType.HEREDOC_STRING,
  675            TokenType.HEX_STRING,
  676            TokenType.IDENTIFIER,
  677            TokenType.NATIONAL_STRING,
  678            TokenType.RAW_STRING,
  679            TokenType.STRING,
  680            TokenType.UNICODE_STRING,
  681        }
  682    )
  683
  684    DB_CREATABLES: t.ClassVar = {
  685        TokenType.DATABASE,
  686        TokenType.DICTIONARY,
  687        TokenType.FILE_FORMAT,
  688        TokenType.MODEL,
  689        TokenType.NAMESPACE,
  690        TokenType.SCHEMA,
  691        TokenType.SEMANTIC_VIEW,
  692        TokenType.SEQUENCE,
  693        TokenType.SINK,
  694        TokenType.SOURCE,
  695        TokenType.STAGE,
  696        TokenType.STORAGE_INTEGRATION,
  697        TokenType.STREAMLIT,
  698        TokenType.TABLE,
  699        TokenType.TAG,
  700        TokenType.VIEW,
  701        TokenType.WAREHOUSE,
  702    }
  703
  704    CREATABLES: t.ClassVar = {
  705        TokenType.COLUMN,
  706        TokenType.CONSTRAINT,
  707        TokenType.FOREIGN_KEY,
  708        TokenType.FUNCTION,
  709        TokenType.INDEX,
  710        TokenType.PROCEDURE,
  711        TokenType.TRIGGER,
  712        TokenType.TYPE,
  713        *DB_CREATABLES,
  714    }
  715
  716    TRIGGER_EVENTS: t.ClassVar = {
  717        TokenType.INSERT,
  718        TokenType.UPDATE,
  719        TokenType.DELETE,
  720        TokenType.TRUNCATE,
  721    }
  722
  723    ALTERABLES: t.ClassVar = {
  724        TokenType.INDEX,
  725        TokenType.TABLE,
  726        TokenType.VIEW,
  727        TokenType.SESSION,
  728    }
  729
  730    # Tokens that can represent identifiers
  731    ID_VAR_TOKENS: t.ClassVar[set] = {
  732        TokenType.ALL,
  733        TokenType.ANALYZE,
  734        TokenType.ATTACH,
  735        TokenType.VAR,
  736        TokenType.ANTI,
  737        TokenType.APPLY,
  738        TokenType.ASC,
  739        TokenType.ASOF,
  740        TokenType.AUTO_INCREMENT,
  741        TokenType.BEGIN,
  742        TokenType.BPCHAR,
  743        TokenType.CACHE,
  744        TokenType.CASE,
  745        TokenType.COLLATE,
  746        TokenType.COMMAND,
  747        TokenType.COMMENT,
  748        TokenType.COMMIT,
  749        TokenType.CONSTRAINT,
  750        TokenType.COPY,
  751        TokenType.CUBE,
  752        TokenType.CURRENT_SCHEMA,
  753        TokenType.DECLARE,
  754        TokenType.DEFAULT,
  755        TokenType.DELETE,
  756        TokenType.DESC,
  757        TokenType.DESCRIBE,
  758        TokenType.DETACH,
  759        TokenType.DICTIONARY,
  760        TokenType.DIV,
  761        TokenType.END,
  762        TokenType.EXECUTE,
  763        TokenType.EXPORT,
  764        TokenType.ESCAPE,
  765        TokenType.FALSE,
  766        TokenType.FIRST,
  767        TokenType.FILE,
  768        TokenType.FILTER,
  769        TokenType.FINAL,
  770        TokenType.FORMAT,
  771        TokenType.FULL,
  772        TokenType.GET,
  773        TokenType.IDENTIFIER,
  774        TokenType.INOUT,
  775        TokenType.IS,
  776        TokenType.ISNULL,
  777        TokenType.INTERVAL,
  778        TokenType.KEEP,
  779        TokenType.KILL,
  780        TokenType.LEFT,
  781        TokenType.LIMIT,
  782        TokenType.LOAD,
  783        TokenType.LOCK,
  784        TokenType.MATCH,
  785        TokenType.MERGE,
  786        TokenType.NATURAL,
  787        TokenType.NEXT,
  788        TokenType.OFFSET,
  789        TokenType.OPERATOR,
  790        TokenType.ORDINALITY,
  791        TokenType.OUT,
  792        TokenType.OVER,
  793        TokenType.OVERLAPS,
  794        TokenType.OVERWRITE,
  795        TokenType.PARTITION,
  796        TokenType.PERCENT,
  797        TokenType.PIVOT,
  798        TokenType.PROJECTION,
  799        TokenType.PRAGMA,
  800        TokenType.PUT,
  801        TokenType.RANGE,
  802        TokenType.RECURSIVE,
  803        TokenType.REFERENCES,
  804        TokenType.REFRESH,
  805        TokenType.RENAME,
  806        TokenType.REPLACE,
  807        TokenType.RIGHT,
  808        TokenType.ROLLUP,
  809        TokenType.ROW,
  810        TokenType.ROWS,
  811        TokenType.SEMI,
  812        TokenType.SET,
  813        TokenType.SETTINGS,
  814        TokenType.SHOW,
  815        TokenType.STREAM,
  816        TokenType.STREAMLIT,
  817        TokenType.TEMPORARY,
  818        TokenType.TOP,
  819        TokenType.TRUE,
  820        TokenType.TRUNCATE,
  821        TokenType.UNIQUE,
  822        TokenType.UNNEST,
  823        TokenType.UNPIVOT,
  824        TokenType.UPDATE,
  825        TokenType.USE,
  826        TokenType.VOLATILE,
  827        TokenType.WINDOW,
  828        TokenType.CURRENT_CATALOG,
  829        TokenType.LOCALTIME,
  830        TokenType.LOCALTIMESTAMP,
  831        TokenType.SESSION_USER,
  832        TokenType.STRAIGHT_JOIN,
  833        *ALTERABLES,
  834        *CREATABLES,
  835        *SUBQUERY_PREDICATES,
  836        *TYPE_TOKENS,
  837        *NO_PAREN_FUNCTIONS,
  838    } - {TokenType.UNION}
  839
  840    TABLE_ALIAS_TOKENS: t.ClassVar[set] = ID_VAR_TOKENS - {
  841        TokenType.ANTI,
  842        TokenType.ASOF,
  843        TokenType.FULL,
  844        TokenType.LEFT,
  845        TokenType.LOCK,
  846        TokenType.NATURAL,
  847        TokenType.RIGHT,
  848        TokenType.SEMI,
  849        TokenType.WINDOW,
  850    }
  851
  852    ALIAS_TOKENS: t.ClassVar = ID_VAR_TOKENS
  853
  854    COLON_PLACEHOLDER_TOKENS: t.ClassVar = ID_VAR_TOKENS
  855
  856    ARRAY_CONSTRUCTORS: t.ClassVar = {
  857        "ARRAY": exp.Array,
  858        "LIST": exp.List,
  859    }
  860
  861    COMMENT_TABLE_ALIAS_TOKENS: t.ClassVar = TABLE_ALIAS_TOKENS - {TokenType.IS}
  862
  863    UPDATE_ALIAS_TOKENS: t.ClassVar = TABLE_ALIAS_TOKENS - {TokenType.SET}
  864
  865    TRIM_TYPES: t.ClassVar = {"LEADING", "TRAILING", "BOTH"}
  866
  867    # Tokens that indicate a simple column reference
  868    IDENTIFIER_TOKENS: t.ClassVar[frozenset] = frozenset({TokenType.VAR, TokenType.IDENTIFIER})
  869
  870    BRACKETS: t.ClassVar[frozenset] = frozenset({TokenType.L_BRACKET, TokenType.L_BRACE})
  871
  872    # Postfix tokens that prevent the bare column fast path
  873    COLUMN_POSTFIX_TOKENS: t.ClassVar[frozenset] = frozenset(
  874        {
  875            TokenType.L_PAREN,
  876            TokenType.L_BRACKET,
  877            TokenType.L_BRACE,
  878            TokenType.COLON,
  879            TokenType.JOIN_MARKER,
  880        }
  881    )
  882
  883    TABLE_POSTFIX_TOKENS: t.ClassVar[frozenset] = frozenset(
  884        {
  885            TokenType.L_PAREN,
  886            TokenType.L_BRACKET,
  887            TokenType.L_BRACE,
  888            TokenType.PIVOT,
  889            TokenType.UNPIVOT,
  890            TokenType.TABLE_SAMPLE,
  891        }
  892    )
  893
  894    FUNC_TOKENS: t.ClassVar = {
  895        TokenType.COLLATE,
  896        TokenType.COMMAND,
  897        TokenType.CURRENT_DATE,
  898        TokenType.CURRENT_DATETIME,
  899        TokenType.CURRENT_SCHEMA,
  900        TokenType.CURRENT_TIMESTAMP,
  901        TokenType.CURRENT_TIME,
  902        TokenType.CURRENT_USER,
  903        TokenType.CURRENT_CATALOG,
  904        TokenType.DECLARE,
  905        TokenType.FILTER,
  906        TokenType.FIRST,
  907        TokenType.FORMAT,
  908        TokenType.GET,
  909        TokenType.GLOB,
  910        TokenType.IDENTIFIER,
  911        TokenType.INDEX,
  912        TokenType.ISNULL,
  913        TokenType.ILIKE,
  914        TokenType.INSERT,
  915        TokenType.LIKE,
  916        TokenType.LOCALTIME,
  917        TokenType.LOCALTIMESTAMP,
  918        TokenType.MERGE,
  919        TokenType.NEXT,
  920        TokenType.OFFSET,
  921        TokenType.PRIMARY_KEY,
  922        TokenType.RANGE,
  923        TokenType.REPLACE,
  924        TokenType.RLIKE,
  925        TokenType.ROW,
  926        TokenType.SESSION_USER,
  927        TokenType.UNNEST,
  928        TokenType.VAR,
  929        TokenType.LEFT,
  930        TokenType.RIGHT,
  931        TokenType.SEQUENCE,
  932        TokenType.DATE,
  933        TokenType.DATETIME,
  934        TokenType.TABLE,
  935        TokenType.TIMESTAMP,
  936        TokenType.TIMESTAMPTZ,
  937        TokenType.TRUNCATE,
  938        TokenType.UTC_DATE,
  939        TokenType.UTC_TIME,
  940        TokenType.UTC_TIMESTAMP,
  941        TokenType.WINDOW,
  942        TokenType.XOR,
  943        *TYPE_TOKENS,
  944        *SUBQUERY_PREDICATES,
  945    }
  946
  947    CONJUNCTION: t.ClassVar[dict[TokenType, type[exp.Expr]]] = {
  948        TokenType.AND: exp.And,
  949    }
  950
  951    ASSIGNMENT: t.ClassVar[dict[TokenType, type[exp.Expr]]] = {
  952        TokenType.COLON_EQ: exp.PropertyEQ,
  953    }
  954
  955    DISJUNCTION: t.ClassVar[dict[TokenType, type[exp.Expr]]] = {
  956        TokenType.OR: exp.Or,
  957    }
  958
  959    EQUALITY: t.ClassVar = {
  960        TokenType.EQ: exp.EQ,
  961        TokenType.NEQ: exp.NEQ,
  962        TokenType.NULLSAFE_EQ: exp.NullSafeEQ,
  963    }
  964
  965    COMPARISON: t.ClassVar = {
  966        TokenType.GT: exp.GT,
  967        TokenType.GTE: exp.GTE,
  968        TokenType.LT: exp.LT,
  969        TokenType.LTE: exp.LTE,
  970    }
  971
  972    BITWISE: t.ClassVar = {
  973        TokenType.AMP: exp.BitwiseAnd,
  974        TokenType.CARET: exp.BitwiseXor,
  975        TokenType.PIPE: exp.BitwiseOr,
  976    }
  977
  978    TERM: t.ClassVar = {
  979        TokenType.DASH: exp.Sub,
  980        TokenType.PLUS: exp.Add,
  981        TokenType.COLLATE: exp.Collate,
  982    }
  983
  984    FACTOR: t.ClassVar = {
  985        TokenType.DIV: exp.IntDiv,
  986        TokenType.LR_ARROW: exp.Distance,
  987        TokenType.LLRR_ARROW: exp.DistanceNd,
  988        TokenType.MOD: exp.Mod,
  989        TokenType.SLASH: exp.Div,
  990        TokenType.STAR: exp.Mul,
  991    }
  992
  993    EXPONENT: t.ClassVar[dict[TokenType, type[exp.Expr]]] = {}
  994
  995    TIMES: t.ClassVar = {
  996        TokenType.TIME,
  997        TokenType.TIMETZ,
  998    }
  999
 1000    TIMESTAMPS: t.ClassVar = {
 1001        TokenType.TIMESTAMP,
 1002        TokenType.TIMESTAMPNTZ,
 1003        TokenType.TIMESTAMPTZ,
 1004        TokenType.TIMESTAMPLTZ,
 1005        *TIMES,
 1006    }
 1007
 1008    SET_OPERATIONS: t.ClassVar = {
 1009        TokenType.UNION,
 1010        TokenType.INTERSECT,
 1011        TokenType.EXCEPT,
 1012    }
 1013
 1014    JOIN_METHODS: t.ClassVar = {
 1015        TokenType.ASOF,
 1016        TokenType.NATURAL,
 1017        TokenType.POSITIONAL,
 1018    }
 1019
 1020    JOIN_SIDES: t.ClassVar = {
 1021        TokenType.LEFT,
 1022        TokenType.RIGHT,
 1023        TokenType.FULL,
 1024    }
 1025
 1026    JOIN_KINDS: t.ClassVar = {
 1027        TokenType.ANTI,
 1028        TokenType.CROSS,
 1029        TokenType.INNER,
 1030        TokenType.OUTER,
 1031        TokenType.SEMI,
 1032        TokenType.STRAIGHT_JOIN,
 1033    }
 1034
 1035    JOIN_HINTS: t.ClassVar[set[str]] = set()
 1036
 1037    # Tokens that unambiguously end a table reference on the fast path
 1038    TABLE_TERMINATORS: t.ClassVar[frozenset] = frozenset(
 1039        {
 1040            TokenType.COMMA,
 1041            TokenType.GROUP_BY,
 1042            TokenType.HAVING,
 1043            TokenType.JOIN,
 1044            TokenType.LIMIT,
 1045            TokenType.ON,
 1046            TokenType.ORDER_BY,
 1047            TokenType.R_PAREN,
 1048            TokenType.SEMICOLON,
 1049            TokenType.SENTINEL,
 1050            TokenType.WHERE,
 1051            *SET_OPERATIONS,
 1052            *JOIN_KINDS,
 1053            *JOIN_METHODS,
 1054            *JOIN_SIDES,
 1055        }
 1056    )
 1057
 1058    LAMBDAS: t.ClassVar = {
 1059        TokenType.ARROW: lambda self, expressions: self.expression(
 1060            exp.Lambda(
 1061                this=self._replace_lambda(
 1062                    self._parse_disjunction(),
 1063                    expressions,
 1064                ),
 1065                expressions=expressions,
 1066            )
 1067        ),
 1068        TokenType.FARROW: lambda self, expressions: self.expression(
 1069            exp.Kwarg(
 1070                this=exp.var(expressions[0].name),
 1071                expression=self._parse_disjunction() or self._parse_select(),
 1072            )
 1073        ),
 1074    }
 1075
 1076    # Whether lambda args include type annotations, e.g. TRANSFORM(arr, x INT -> x + 1) in Snowflake
 1077    TYPED_LAMBDA_ARGS: t.ClassVar[bool] = False
 1078
 1079    LAMBDA_ARG_TERMINATORS: t.ClassVar[frozenset] = frozenset({TokenType.COMMA, TokenType.R_PAREN})
 1080
 1081    COLUMN_OPERATORS: t.ClassVar = {
 1082        TokenType.DOT: None,
 1083        TokenType.DOTCOLON: lambda self, this, to: self.expression(exp.JSONCast(this=this, to=to)),
 1084        TokenType.DCOLON: lambda self, this, to: self.build_cast(
 1085            strict=self.STRICT_CAST, this=this, to=to
 1086        ),
 1087        TokenType.ARROW: lambda self, this, path: self.expression(
 1088            exp.JSONExtract(
 1089                this=this,
 1090                expression=self.dialect.to_json_path(path),
 1091                only_json_types=self.JSON_ARROWS_REQUIRE_JSON_TYPE,
 1092            )
 1093        ),
 1094        TokenType.DARROW: lambda self, this, path: self.expression(
 1095            exp.JSONExtractScalar(
 1096                this=this,
 1097                expression=self.dialect.to_json_path(path),
 1098                only_json_types=self.JSON_ARROWS_REQUIRE_JSON_TYPE,
 1099                scalar_only=self.dialect.JSON_EXTRACT_SCALAR_SCALAR_ONLY,
 1100            )
 1101        ),
 1102        TokenType.HASH_ARROW: lambda self, this, path: self.expression(
 1103            exp.JSONBExtract(this=this, expression=path)
 1104        ),
 1105        TokenType.DHASH_ARROW: lambda self, this, path: self.expression(
 1106            exp.JSONBExtractScalar(this=this, expression=path)
 1107        ),
 1108        TokenType.PLACEHOLDER: lambda self, this, key: self.expression(
 1109            exp.JSONBContainsTopKey(this=this, expression=key)
 1110        ),
 1111    }
 1112
 1113    # JSON/JSONB operators (extraction and containment) at Postgres's "any other operator"
 1114    # tier, below +/-, level with ||. Same value signature as COLUMN_OPERATORS: (self, this, rhs).
 1115    JSON_OPERATORS: t.ClassVar[dict[TokenType, t.Callable]] = {}
 1116
 1117    CAST_COLUMN_OPERATORS: t.ClassVar = {
 1118        TokenType.DOTCOLON,
 1119        TokenType.DCOLON,
 1120    }
 1121
 1122    EXPRESSION_PARSERS: t.ClassVar = {
 1123        exp.Cluster: lambda self: self._parse_sort(exp.Cluster, TokenType.CLUSTER_BY),
 1124        exp.Column: lambda self: self._parse_column(),
 1125        exp.ColumnDef: lambda self: self._parse_column_def(self._parse_column()),
 1126        exp.Condition: lambda self: self._parse_disjunction(),
 1127        exp.DataType: lambda self: self._parse_types(allow_identifiers=False, schema=True),
 1128        exp.Expr: lambda self: self._parse_expression(),
 1129        exp.From: lambda self: self._parse_from(joins=True),
 1130        exp.GrantPrincipal: lambda self: self._parse_grant_principal(),
 1131        exp.GrantPrivilege: lambda self: self._parse_grant_privilege(),
 1132        exp.Group: lambda self: self._parse_group(),
 1133        exp.Having: lambda self: self._parse_having(),
 1134        exp.Hint: lambda self: self._parse_hint_body(),
 1135        exp.Identifier: lambda self: self._parse_id_var(),
 1136        exp.Join: lambda self: self._parse_join(),
 1137        exp.Lambda: lambda self: self._parse_lambda(),
 1138        exp.Lateral: lambda self: self._parse_lateral(),
 1139        exp.Limit: lambda self: self._parse_limit(),
 1140        exp.Offset: lambda self: self._parse_offset(),
 1141        exp.Order: lambda self: self._parse_order(),
 1142        exp.Ordered: lambda self: self._parse_ordered(),
 1143        exp.Properties: lambda self: self._parse_properties(),
 1144        exp.PartitionedByProperty: lambda self: self._parse_partitioned_by(),
 1145        exp.Qualify: lambda self: self._parse_qualify(),
 1146        exp.Returning: lambda self: self._parse_returning(),
 1147        exp.Select: lambda self: self._parse_select(),
 1148        exp.Sort: lambda self: self._parse_sort(exp.Sort, TokenType.SORT_BY),
 1149        exp.Table: lambda self: self._parse_table_parts(),
 1150        exp.TableAlias: lambda self: self._parse_table_alias(),
 1151        exp.Tuple: lambda self: self._parse_value(values=False),
 1152        exp.Whens: lambda self: self._parse_when_matched(),
 1153        exp.Where: lambda self: self._parse_where(),
 1154        exp.Window: lambda self: self._parse_named_window(),
 1155        exp.With: lambda self: self._parse_with(),
 1156    }
 1157
 1158    STATEMENT_PARSERS: t.ClassVar = {
 1159        TokenType.ALTER: lambda self: self._parse_alter(),
 1160        TokenType.ANALYZE: lambda self: self._parse_analyze(),
 1161        TokenType.BEGIN: lambda self: self._parse_transaction(),
 1162        TokenType.CACHE: lambda self: self._parse_cache(),
 1163        TokenType.COMMENT: lambda self: self._parse_comment(),
 1164        TokenType.COMMIT: lambda self: self._parse_commit_or_rollback(),
 1165        TokenType.COPY: lambda self: self._parse_copy(),
 1166        TokenType.CREATE: lambda self: self._parse_create(),
 1167        TokenType.DECLARE: lambda self: self._parse_declare(),
 1168        TokenType.DELETE: lambda self: self._parse_delete(),
 1169        TokenType.DESC: lambda self: self._parse_describe(),
 1170        TokenType.DESCRIBE: lambda self: self._parse_describe(),
 1171        TokenType.DROP: lambda self: self._parse_drop(),
 1172        TokenType.GRANT: lambda self: self._parse_grant(),
 1173        TokenType.REVOKE: lambda self: self._parse_revoke(),
 1174        TokenType.INSERT: lambda self: self._parse_insert(),
 1175        TokenType.KILL: lambda self: self._parse_kill(),
 1176        TokenType.LOAD: lambda self: self._parse_load(),
 1177        TokenType.MERGE: lambda self: self._parse_merge(),
 1178        TokenType.PIVOT: lambda self: self._parse_simplified_pivot(),
 1179        TokenType.PRAGMA: lambda self: self.expression(exp.Pragma(this=self._parse_expression())),
 1180        TokenType.REFRESH: lambda self: self._parse_refresh(),
 1181        TokenType.ROLLBACK: lambda self: self._parse_commit_or_rollback(),
 1182        TokenType.SET: lambda self: self._parse_set(),
 1183        TokenType.TRUNCATE: lambda self: self._parse_truncate_table(),
 1184        TokenType.UNCACHE: lambda self: self._parse_uncache(),
 1185        TokenType.UNPIVOT: lambda self: self._parse_simplified_pivot(is_unpivot=True),
 1186        TokenType.UPDATE: lambda self: self._parse_update(),
 1187        TokenType.USE: lambda self: self._parse_use(),
 1188        TokenType.SEMICOLON: lambda self: exp.Semicolon(),
 1189    }
 1190
 1191    UNARY_PARSERS: t.ClassVar = {
 1192        TokenType.PLUS: lambda self: self._parse_unary(),  # Unary + is handled as a no-op
 1193        TokenType.NOT: lambda self: self.expression(exp.Not(this=self._parse_equality())),
 1194        TokenType.TILDE: lambda self: self.expression(exp.BitwiseNot(this=self._parse_unary())),
 1195        TokenType.DASH: lambda self: self.expression(exp.Neg(this=self._parse_unary())),
 1196        TokenType.PIPE_SLASH: lambda self: self.expression(exp.Sqrt(this=self._parse_unary())),
 1197        TokenType.DPIPE_SLASH: lambda self: self.expression(exp.Cbrt(this=self._parse_unary())),
 1198    }
 1199
 1200    STRING_PARSERS: t.ClassVar = {
 1201        TokenType.HEREDOC_STRING: lambda self, token: self.expression(
 1202            exp.RawString(this=token.text), token
 1203        ),
 1204        TokenType.NATIONAL_STRING: lambda self, token: self.expression(
 1205            exp.National(this=token.text), token
 1206        ),
 1207        TokenType.RAW_STRING: lambda self, token: self.expression(
 1208            exp.RawString(this=token.text), token
 1209        ),
 1210        TokenType.STRING: lambda self, token: self.expression(
 1211            exp.Literal(this=token.text, is_string=True), token
 1212        ),
 1213        TokenType.UNICODE_STRING: lambda self, token: self.expression(
 1214            exp.UnicodeString(
 1215                this=token.text, escape=self._match_text_seq("UESCAPE") and self._parse_string()
 1216            ),
 1217            token,
 1218        ),
 1219    }
 1220
 1221    NUMERIC_PARSERS: t.ClassVar = {
 1222        TokenType.BIT_STRING: lambda self, token: self.expression(
 1223            exp.BitString(this=token.text), token
 1224        ),
 1225        TokenType.BYTE_STRING: lambda self, token: self.expression(
 1226            exp.ByteString(
 1227                this=token.text, is_bytes=self.dialect.BYTE_STRING_IS_BYTES_TYPE or None
 1228            ),
 1229            token,
 1230        ),
 1231        TokenType.HEX_STRING: lambda self, token: self.expression(
 1232            exp.HexString(
 1233                this=token.text, is_integer=self.dialect.HEX_STRING_IS_INTEGER_TYPE or None
 1234            ),
 1235            token,
 1236        ),
 1237        TokenType.NUMBER: lambda self, token: self.expression(
 1238            exp.Literal(this=token.text, is_string=False), token
 1239        ),
 1240    }
 1241
 1242    PRIMARY_PARSERS: t.ClassVar = {
 1243        **STRING_PARSERS,
 1244        **NUMERIC_PARSERS,
 1245        TokenType.INTRODUCER: lambda self, token: self._parse_introducer(token),
 1246        TokenType.NULL: lambda self, _: self.expression(exp.Null()),
 1247        TokenType.TRUE: lambda self, _: self.expression(exp.Boolean(this=True)),
 1248        TokenType.FALSE: lambda self, _: self.expression(exp.Boolean(this=False)),
 1249        TokenType.SESSION_PARAMETER: lambda self, _: self._parse_session_parameter(),
 1250        TokenType.STAR: lambda self, _: self._parse_star_ops(),
 1251    }
 1252
 1253    PLACEHOLDER_PARSERS: t.ClassVar = {
 1254        TokenType.PLACEHOLDER: lambda self: self.expression(exp.Placeholder()),
 1255        TokenType.PARAMETER: lambda self: self._parse_parameter(),
 1256        TokenType.COLON: lambda self: (
 1257            self.expression(exp.Placeholder(this=self._prev.text))
 1258            if self._match_set(self.COLON_PLACEHOLDER_TOKENS)
 1259            else None
 1260        ),
 1261    }
 1262
 1263    RANGE_PARSERS: t.ClassVar = {
 1264        TokenType.AT_GT: binary_range_parser(exp.ArrayContainsAll),
 1265        TokenType.BETWEEN: lambda self, this: self._parse_between(this),
 1266        TokenType.GLOB: binary_range_parser(exp.Glob),
 1267        TokenType.ILIKE: binary_range_parser(exp.ILike),
 1268        TokenType.IN: lambda self, this: self._parse_in(this),
 1269        TokenType.IRLIKE: binary_range_parser(exp.RegexpILike),
 1270        TokenType.IS: lambda self, this: self._parse_is(this),
 1271        TokenType.LIKE: binary_range_parser(exp.Like),
 1272        TokenType.LT_AT: binary_range_parser(exp.ArrayContainedBy),
 1273        TokenType.OVERLAPS: binary_range_parser(exp.Overlaps),
 1274        TokenType.RLIKE: binary_range_parser(exp.RegexpLike),
 1275        TokenType.SIMILAR_TO: binary_range_parser(exp.SimilarTo),
 1276        TokenType.FOR: lambda self, this: self._parse_comprehension(this),
 1277        TokenType.QMARK_AMP: binary_range_parser(exp.JSONBContainsAllTopKeys),
 1278        TokenType.QMARK_PIPE: binary_range_parser(exp.JSONBContainsAnyTopKeys),
 1279        TokenType.HASH_DASH: binary_range_parser(exp.JSONBDeleteAtPath),
 1280        TokenType.AT_QMARK: binary_range_parser(exp.JSONBPathExists),
 1281        TokenType.ADJACENT: binary_range_parser(exp.Adjacent),
 1282        TokenType.OPERATOR: lambda self, this: self._parse_operator(this),
 1283        TokenType.AMP_LT: binary_range_parser(exp.ExtendsLeft),
 1284        TokenType.AMP_GT: binary_range_parser(exp.ExtendsRight),
 1285        TokenType.CARET_AT: binary_range_parser(exp.StartsWith),
 1286    }
 1287
 1288    PIPE_SYNTAX_TRANSFORM_PARSERS: t.ClassVar = {
 1289        "AGGREGATE": lambda self, query: self._parse_pipe_syntax_aggregate(query),
 1290        "AS": lambda self, query: self._build_pipe_cte(
 1291            query, [exp.Star()], self._parse_table_alias()
 1292        ),
 1293        "DISTINCT": lambda self, query: self._advance() or query.distinct(copy=False),
 1294        "EXTEND": lambda self, query: self._parse_pipe_syntax_extend(query),
 1295        "LIMIT": lambda self, query: self._parse_pipe_syntax_limit(query),
 1296        "ORDER BY": lambda self, query: query.order_by(
 1297            self._parse_order(), append=False, copy=False
 1298        ),
 1299        "PIVOT": lambda self, query: self._parse_pipe_syntax_pivot(query),
 1300        "SELECT": lambda self, query: self._parse_pipe_syntax_select(query),
 1301        "TABLESAMPLE": lambda self, query: self._parse_pipe_syntax_tablesample(query),
 1302        "UNPIVOT": lambda self, query: self._parse_pipe_syntax_pivot(query),
 1303        "WHERE": lambda self, query: query.where(self._parse_where(), copy=False),
 1304    }
 1305
 1306    PROPERTY_PARSERS: t.ClassVar[dict[str, t.Callable]] = {
 1307        "ALLOWED_VALUES": lambda self: self.expression(
 1308            exp.AllowedValuesProperty(expressions=self._parse_csv(self._parse_primary))
 1309        ),
 1310        "ALGORITHM": lambda self: self._parse_property_assignment(exp.AlgorithmProperty),
 1311        "AUTO": lambda self: self._parse_auto_property(),
 1312        "AUTO_INCREMENT": lambda self: self._parse_property_assignment(exp.AutoIncrementProperty),
 1313        "BACKUP": lambda self: self.expression(
 1314            exp.BackupProperty(this=self._parse_var(any_token=True))
 1315        ),
 1316        "BLOCKCOMPRESSION": lambda self: self._parse_blockcompression(),
 1317        "CALLED": lambda self: self._parse_called_on_null_input_property(),
 1318        "CHARSET": lambda self, **kwargs: self._parse_character_set(**kwargs),
 1319        "CHECKSUM": lambda self: self._parse_checksum(),
 1320        "CLUSTER BY": lambda self: self._parse_cluster_property(),
 1321        "CLUSTERED": lambda self: self._parse_clustered_by(),
 1322        "COLLATE": lambda self, **kwargs: self._parse_property_assignment(
 1323            exp.CollateProperty, **kwargs
 1324        ),
 1325        "COMMENT": lambda self: self._parse_property_assignment(exp.SchemaCommentProperty),
 1326        "CONTAINS": lambda self: self._parse_contains_property(),
 1327        "COPY": lambda self: self._parse_copy_property(),
 1328        "DATABLOCKSIZE": lambda self, **kwargs: self._parse_datablocksize(**kwargs),
 1329        "DATA_DELETION": lambda self: self._parse_data_deletion_property(),
 1330        "DEFINER": lambda self: self._parse_definer(),
 1331        "DETERMINISTIC": lambda self: self.expression(
 1332            exp.StabilityProperty(this=exp.Literal.string("IMMUTABLE"))
 1333        ),
 1334        "DISTRIBUTED": lambda self: self._parse_distributed_property(),
 1335        "DUPLICATE": lambda self: self._parse_composite_key_property(exp.DuplicateKeyProperty),
 1336        "DYNAMIC": lambda self: self.expression(exp.DynamicProperty()),
 1337        "DISTKEY": lambda self: self._parse_distkey(),
 1338        "DISTSTYLE": lambda self: self._parse_property_assignment(exp.DistStyleProperty),
 1339        "EMPTY": lambda self: self.expression(exp.EmptyProperty()),
 1340        "ENGINE": lambda self: self._parse_property_assignment(exp.EngineProperty),
 1341        "ENVIRONMENT": lambda self: self.expression(
 1342            exp.EnviromentProperty(expressions=self._parse_wrapped_csv(self._parse_assignment))
 1343        ),
 1344        "HANDLER": lambda self: self._parse_property_assignment(exp.HandlerProperty),
 1345        "EXECUTE": lambda self: self._parse_property_assignment(exp.ExecuteAsProperty),
 1346        "EXTERNAL": lambda self: self.expression(exp.ExternalProperty()),
 1347        "FALLBACK": lambda self, **kwargs: self._parse_fallback(**kwargs),
 1348        "FORMAT": lambda self: self._parse_property_assignment(exp.FileFormatProperty),
 1349        "FREESPACE": lambda self: self._parse_freespace(),
 1350        "GLOBAL": lambda self: self.expression(exp.GlobalProperty()),
 1351        "HEAP": lambda self: self.expression(exp.HeapProperty()),
 1352        "ICEBERG": lambda self: self.expression(exp.IcebergProperty()),
 1353        "IMMUTABLE": lambda self: self.expression(
 1354            exp.StabilityProperty(this=exp.Literal.string("IMMUTABLE"))
 1355        ),
 1356        "INHERITS": lambda self: self.expression(
 1357            exp.InheritsProperty(expressions=self._parse_wrapped_csv(self._parse_table))
 1358        ),
 1359        "INPUT": lambda self: self.expression(exp.InputModelProperty(this=self._parse_schema())),
 1360        "JOURNAL": lambda self, **kwargs: self._parse_journal(**kwargs),
 1361        "LANGUAGE": lambda self: self._parse_property_assignment(exp.LanguageProperty),
 1362        "LAYOUT": lambda self: self._parse_dict_property(this="LAYOUT"),
 1363        "LIFETIME": lambda self: self._parse_dict_range(this="LIFETIME"),
 1364        "LIKE": lambda self: self._parse_create_like(),
 1365        "LOCATION": lambda self: self._parse_property_assignment(exp.LocationProperty),
 1366        "LOCK": lambda self: self._parse_locking(),
 1367        "LOCKING": lambda self: self._parse_locking(),
 1368        "LOG": lambda self, **kwargs: self._parse_log(**kwargs),
 1369        "MATERIALIZED": lambda self: self.expression(exp.MaterializedProperty()),
 1370        "MERGEBLOCKRATIO": lambda self, **kwargs: self._parse_mergeblockratio(**kwargs),
 1371        "MODIFIES": lambda self: self._parse_modifies_property(),
 1372        "MULTISET": lambda self: self.expression(exp.SetProperty(multi=True)),
 1373        "NO": lambda self: self._parse_no_property(),
 1374        "ON": lambda self: self._parse_on_property(),
 1375        "ORDER BY": lambda self: self._parse_order(skip_order_token=True),
 1376        "OUTPUT": lambda self: self.expression(exp.OutputModelProperty(this=self._parse_schema())),
 1377        "PARTITION": lambda self: self._parse_partitioned_of(),
 1378        "PARTITION BY": lambda self: self._parse_partitioned_by(),
 1379        "PARTITIONED BY": lambda self: self._parse_partitioned_by(),
 1380        "PARTITIONED_BY": lambda self: self._parse_partitioned_by(),
 1381        "PRIMARY KEY": lambda self: self._parse_primary_key(in_props=True),
 1382        "RANGE": lambda self: self._parse_dict_range(this="RANGE"),
 1383        "READS": lambda self: self._parse_reads_property(),
 1384        "REMOTE": lambda self: self._parse_remote_with_connection(),
 1385        "RETURNS": lambda self: self._parse_returns(),
 1386        "STRICT": lambda self: self.expression(exp.StrictProperty()),
 1387        "STREAMING": lambda self: self.expression(exp.StreamingTableProperty()),
 1388        "ROW": lambda self: self._parse_row(),
 1389        "ROW_FORMAT": lambda self: self._parse_property_assignment(exp.RowFormatProperty),
 1390        "SAMPLE": lambda self: self.expression(
 1391            exp.SampleProperty(this=self._match_text_seq("BY") and self._parse_bitwise())
 1392        ),
 1393        "SECURE": lambda self: self.expression(exp.SecureProperty()),
 1394        "SECURITY": lambda self: self._parse_sql_security(),
 1395        "SQL SECURITY": lambda self: self._parse_sql_security(),
 1396        "SET": lambda self: self.expression(exp.SetProperty(multi=False)),
 1397        "SETTINGS": lambda self: self._parse_settings_property(),
 1398        "SHARING": lambda self: self._parse_property_assignment(exp.SharingProperty),
 1399        "SORTKEY": lambda self: self._parse_sortkey(),
 1400        "SOURCE": lambda self: self._parse_dict_property(this="SOURCE"),
 1401        "STABLE": lambda self: self.expression(
 1402            exp.StabilityProperty(this=exp.Literal.string("STABLE"))
 1403        ),
 1404        "STORED": lambda self: self._parse_stored(),
 1405        "SYSTEM_VERSIONING": lambda self: self._parse_system_versioning_property(),
 1406        "TBLPROPERTIES": lambda self: self._parse_wrapped_properties(),
 1407        "TEMP": lambda self: self.expression(exp.TemporaryProperty()),
 1408        "TEMPORARY": lambda self: self.expression(exp.TemporaryProperty()),
 1409        "TO": lambda self: self._parse_to_table(),
 1410        "TRANSIENT": lambda self: self.expression(exp.TransientProperty()),
 1411        "TRANSFORM": lambda self: self.expression(
 1412            exp.TransformModelProperty(expressions=self._parse_wrapped_csv(self._parse_expression))
 1413        ),
 1414        "TTL": lambda self: self._parse_ttl(),
 1415        "USING": lambda self: self._parse_property_assignment(exp.FileFormatProperty),
 1416        "UNLOGGED": lambda self: self.expression(exp.UnloggedProperty()),
 1417        "VOLATILE": lambda self: self._parse_volatile_property(),
 1418        "WITH": lambda self: self._parse_with_property(),
 1419    }
 1420
 1421    CONSTRAINT_PARSERS: t.ClassVar = {
 1422        "AUTOINCREMENT": lambda self: self._parse_auto_increment(),
 1423        "AUTO_INCREMENT": lambda self: self._parse_auto_increment(),
 1424        "CASESPECIFIC": lambda self: self.expression(exp.CaseSpecificColumnConstraint(not_=False)),
 1425        "CHECK": lambda self: self._parse_check_constraint(),
 1426        "COLLATE": lambda self: self.expression(
 1427            exp.CollateColumnConstraint(this=self._parse_identifier() or self._parse_column())
 1428        ),
 1429        "COMMENT": lambda self: self.expression(
 1430            exp.CommentColumnConstraint(this=self._parse_string())
 1431        ),
 1432        "COMPRESS": lambda self: self._parse_compress(),
 1433        "CLUSTERED": lambda self: self.expression(
 1434            exp.ClusteredColumnConstraint(this=self._parse_wrapped_csv(self._parse_ordered))
 1435        ),
 1436        "NONCLUSTERED": lambda self: self.expression(
 1437            exp.NonClusteredColumnConstraint(this=self._parse_wrapped_csv(self._parse_ordered))
 1438        ),
 1439        "DEFAULT": lambda self: self.expression(
 1440            exp.DefaultColumnConstraint(this=self._parse_bitwise())
 1441        ),
 1442        "ENCODE": lambda self: self.expression(exp.EncodeColumnConstraint(this=self._parse_var())),
 1443        "EPHEMERAL": lambda self: self.expression(
 1444            exp.EphemeralColumnConstraint(this=self._parse_bitwise())
 1445        ),
 1446        "EXCLUDE": lambda self: self.expression(
 1447            exp.ExcludeColumnConstraint(this=self._parse_index_params())
 1448        ),
 1449        "FOREIGN KEY": lambda self: self._parse_foreign_key(),
 1450        "FORMAT": lambda self: self.expression(
 1451            exp.DateFormatColumnConstraint(this=self._parse_var_or_string())
 1452        ),
 1453        "GENERATED": lambda self: self._parse_generated_as_identity(),
 1454        "IDENTITY": lambda self: self._parse_auto_increment(),
 1455        "INLINE": lambda self: self._parse_inline(),
 1456        "LIKE": lambda self: self._parse_create_like(),
 1457        "NOT": lambda self: self._parse_not_constraint(),
 1458        "NULL": lambda self: self.expression(exp.NotNullColumnConstraint(allow_null=True)),
 1459        "ON": lambda self: (
 1460            (
 1461                self._match(TokenType.UPDATE)
 1462                and self.expression(exp.OnUpdateColumnConstraint(this=self._parse_function()))
 1463            )
 1464            or self.expression(exp.OnProperty(this=self._parse_id_var()))
 1465        ),
 1466        "PATH": lambda self: self.expression(exp.PathColumnConstraint(this=self._parse_string())),
 1467        "PERIOD": lambda self: self._parse_period_for_system_time(),
 1468        "PRIMARY KEY": lambda self: self._parse_primary_key(),
 1469        "REFERENCES": lambda self: self._parse_references(match=False),
 1470        "TITLE": lambda self: self.expression(
 1471            exp.TitleColumnConstraint(this=self._parse_var_or_string())
 1472        ),
 1473        "TTL": lambda self: self.expression(exp.MergeTreeTTL(expressions=[self._parse_bitwise()])),
 1474        "UNIQUE": lambda self: self._parse_unique(),
 1475        "UPPERCASE": lambda self: self.expression(exp.UppercaseColumnConstraint()),
 1476        "WITH": lambda self: self.expression(
 1477            exp.Properties(expressions=self._parse_wrapped_properties())
 1478        ),
 1479        "BUCKET": lambda self: self._parse_partitioned_by_bucket_or_truncate(),
 1480        "TRUNCATE": lambda self: self._parse_partitioned_by_bucket_or_truncate(),
 1481    }
 1482
 1483    def _parse_partitioned_by_bucket_or_truncate(self) -> exp.Expr | None:
 1484        if not self._match(TokenType.L_PAREN, advance=False):
 1485            # Partitioning by bucket or truncate follows the syntax:
 1486            # PARTITION BY (BUCKET(..) | TRUNCATE(..))
 1487            # If we don't have parenthesis after each keyword, we should instead parse this as an identifier
 1488            self._retreat(self._index - 1)
 1489            return None
 1490
 1491        klass = (
 1492            exp.PartitionedByBucket
 1493            if self._prev.text.upper() == "BUCKET"
 1494            else exp.PartitionByTruncate
 1495        )
 1496
 1497        args = self._parse_wrapped_csv(lambda: self._parse_primary() or self._parse_column())
 1498        this, expression = seq_get(args, 0), seq_get(args, 1)
 1499
 1500        if isinstance(this, exp.Literal):
 1501            # Check for Iceberg partition transforms (bucket / truncate) and ensure their arguments are in the right order
 1502            #  - For Hive, it's `bucket(<num buckets>, <col name>)` or `truncate(<num_chars>, <col_name>)`
 1503            #  - For Trino, it's reversed - `bucket(<col name>, <num buckets>)` or `truncate(<col_name>, <num_chars>)`
 1504            # Both variants are canonicalized in the latter i.e `bucket(<col name>, <num buckets>)`
 1505            #
 1506            # Hive ref: https://docs.aws.amazon.com/athena/latest/ug/querying-iceberg-creating-tables.html#querying-iceberg-partitioning
 1507            # Trino ref: https://docs.aws.amazon.com/athena/latest/ug/create-table-as.html#ctas-table-properties
 1508            this, expression = expression, this
 1509
 1510        return self.expression(klass(this=this, expression=expression))
 1511
 1512    ALTER_PARSERS: t.ClassVar = {
 1513        "ADD": lambda self: self._parse_alter_table_add(),
 1514        "AS": lambda self: self._parse_select(),
 1515        "ALTER": lambda self: self._parse_alter_table_alter(),
 1516        "CLUSTER BY": lambda self: self._parse_cluster_property(),
 1517        "DELETE": lambda self: self.expression(exp.Delete(where=self._parse_where())),
 1518        "DROP": lambda self: self._parse_alter_table_drop(),
 1519        "RENAME": lambda self: self._parse_alter_table_rename(),
 1520        "SET": lambda self: self._parse_alter_table_set(),
 1521        "SWAP": lambda self: self.expression(
 1522            exp.SwapTable(this=self._match(TokenType.WITH) and self._parse_table(schema=True))
 1523        ),
 1524    }
 1525
 1526    ALTER_ALTER_PARSERS: t.ClassVar = {
 1527        "DISTKEY": lambda self: self._parse_alter_diststyle(),
 1528        "DISTSTYLE": lambda self: self._parse_alter_diststyle(),
 1529        "SORTKEY": lambda self: self._parse_alter_sortkey(),
 1530        "COMPOUND": lambda self: self._parse_alter_sortkey(compound=True),
 1531    }
 1532
 1533    SCHEMA_UNNAMED_CONSTRAINTS: t.ClassVar = {
 1534        "CHECK",
 1535        "EXCLUDE",
 1536        "FOREIGN KEY",
 1537        "LIKE",
 1538        "PERIOD",
 1539        "PRIMARY KEY",
 1540        "UNIQUE",
 1541        "BUCKET",
 1542        "TRUNCATE",
 1543    }
 1544
 1545    NO_PAREN_FUNCTION_PARSERS: t.ClassVar = {
 1546        "ANY": lambda self: self.expression(exp.Any(this=self._parse_bitwise())),
 1547        "CASE": lambda self: self._parse_case(),
 1548        "CONNECT_BY_ROOT": lambda self: self.expression(
 1549            exp.ConnectByRoot(this=self._parse_column())
 1550        ),
 1551        "IF": lambda self: self._parse_if(),
 1552    }
 1553
 1554    INVALID_FUNC_NAME_TOKENS: t.ClassVar = {
 1555        TokenType.IDENTIFIER,
 1556        TokenType.STRING,
 1557    }
 1558
 1559    FUNCTIONS_WITH_ALIASED_ARGS: t.ClassVar = {"STRUCT"}
 1560
 1561    KEY_VALUE_DEFINITIONS: t.ClassVar = (exp.Alias, exp.EQ, exp.PropertyEQ, exp.Slice)
 1562
 1563    FUNCTION_PARSERS: t.ClassVar[dict[str, t.Callable]] = {
 1564        **{
 1565            name: lambda self: self._parse_distinct_arg_function(exp.ArgMax)
 1566            for name in exp.ArgMax.sql_names()
 1567        },
 1568        **{
 1569            name: lambda self: self._parse_distinct_arg_function(exp.ArgMin)
 1570            for name in exp.ArgMin.sql_names()
 1571        },
 1572        "CAST": lambda self: self._parse_cast(self.STRICT_CAST),
 1573        "CEIL": lambda self: self._parse_ceil_floor(exp.Ceil),
 1574        "CONVERT": lambda self: self._parse_convert(self.STRICT_CAST),
 1575        "CHAR": lambda self: self._parse_char(),
 1576        "CHR": lambda self: self._parse_char(),
 1577        "DECODE": lambda self: self._parse_decode(),
 1578        "EXTRACT": lambda self: self._parse_extract(),
 1579        "FLOOR": lambda self: self._parse_ceil_floor(exp.Floor),
 1580        "GAP_FILL": lambda self: self._parse_gap_fill(),
 1581        "INITCAP": lambda self: self._parse_initcap(),
 1582        "JSON_OBJECT": lambda self: self._parse_json_object(),
 1583        "JSON_OBJECTAGG": lambda self: self._parse_json_object(agg=True),
 1584        "JSON_TABLE": lambda self: self._parse_json_table(),
 1585        "MATCH": lambda self: self._parse_match_against(),
 1586        "NORMALIZE": lambda self: self._parse_normalize(),
 1587        "OPENJSON": lambda self: self._parse_open_json(),
 1588        "OVERLAY": lambda self: self._parse_overlay(),
 1589        "POSITION": lambda self: self._parse_position(),
 1590        "SAFE_CAST": lambda self: self._parse_cast(False, safe=True),
 1591        "STRING_AGG": lambda self: self._parse_string_agg(),
 1592        "SUBSTRING": lambda self: self._parse_substring(),
 1593        "TRIM": lambda self: self._parse_trim(),
 1594        "TRY_CAST": lambda self: self._parse_cast(False, safe=True),
 1595        "TRY_CONVERT": lambda self: self._parse_convert(False, safe=True),
 1596        "WEIGHT_STRING": lambda self: self._parse_weight_string(),
 1597        "XMLELEMENT": lambda self: self._parse_xml_element(),
 1598        "XMLTABLE": lambda self: self._parse_xml_table(),
 1599    }
 1600
 1601    QUERY_MODIFIER_PARSERS: t.ClassVar = {
 1602        TokenType.MATCH_RECOGNIZE: lambda self: ("match", self._parse_match_recognize()),
 1603        TokenType.PREWHERE: lambda self: ("prewhere", self._parse_prewhere()),
 1604        TokenType.WHERE: lambda self: ("where", self._parse_where()),
 1605        TokenType.GROUP_BY: lambda self: ("group", self._parse_group()),
 1606        TokenType.HAVING: lambda self: ("having", self._parse_having()),
 1607        TokenType.QUALIFY: lambda self: ("qualify", self._parse_qualify()),
 1608        TokenType.WINDOW: lambda self: ("windows", self._parse_window_clause()),
 1609        TokenType.ORDER_BY: lambda self: ("order", self._parse_order()),
 1610        TokenType.LIMIT: lambda self: ("limit", self._parse_limit()),
 1611        TokenType.FETCH: lambda self: ("limit", self._parse_limit()),
 1612        TokenType.OFFSET: lambda self: ("offset", self._parse_offset()),
 1613        TokenType.FOR: lambda self: ("locks", self._parse_locks()),
 1614        TokenType.LOCK: lambda self: ("locks", self._parse_locks()),
 1615        TokenType.TABLE_SAMPLE: lambda self: ("sample", self._parse_table_sample(as_modifier=True)),
 1616        TokenType.USING: lambda self: ("sample", self._parse_table_sample(as_modifier=True)),
 1617        TokenType.CLUSTER_BY: lambda self: (
 1618            "cluster",
 1619            self._parse_cluster(),
 1620        ),
 1621        TokenType.DISTRIBUTE_BY: lambda self: (
 1622            "distribute",
 1623            self._parse_sort(exp.Distribute, TokenType.DISTRIBUTE_BY),
 1624        ),
 1625        TokenType.SORT_BY: lambda self: ("sort", self._parse_sort(exp.Sort, TokenType.SORT_BY)),
 1626        TokenType.CONNECT_BY: lambda self: ("connect", self._parse_connect(skip_start_token=True)),
 1627    }
 1628    QUERY_MODIFIER_TOKENS: t.ClassVar = set(QUERY_MODIFIER_PARSERS)
 1629
 1630    SET_PARSERS: t.ClassVar = {
 1631        "GLOBAL": lambda self: self._parse_set_item_assignment("GLOBAL"),
 1632        "LOCAL": lambda self: self._parse_set_item_assignment("LOCAL"),
 1633        "SESSION": lambda self: self._parse_set_item_assignment("SESSION"),
 1634        "TRANSACTION": lambda self: self._parse_set_transaction(),
 1635    }
 1636
 1637    SHOW_PARSERS: t.ClassVar[dict[str, t.Callable]] = {}
 1638
 1639    TYPE_LITERAL_PARSERS: t.ClassVar = {
 1640        exp.DType.JSON: lambda self, this, _: self.expression(
 1641            exp.ParseJSON(this=this, is_literal=True)
 1642        ),
 1643    }
 1644
 1645    TYPE_CONVERTERS: t.ClassVar[dict[exp.DType, t.Callable[[exp.DataType], exp.DataType]]] = {}
 1646
 1647    DDL_SELECT_TOKENS: t.ClassVar = {TokenType.SELECT, TokenType.WITH, TokenType.L_PAREN}
 1648
 1649    PRE_VOLATILE_TOKENS: t.ClassVar = {TokenType.CREATE, TokenType.REPLACE, TokenType.UNIQUE}
 1650
 1651    TRANSACTION_KIND: t.ClassVar = {"DEFERRED", "IMMEDIATE", "EXCLUSIVE"}
 1652    TRANSACTION_CHARACTERISTICS: t.ClassVar[OPTIONS_TYPE] = {
 1653        "ISOLATION": (
 1654            ("LEVEL", "REPEATABLE", "READ"),
 1655            ("LEVEL", "READ", "COMMITTED"),
 1656            ("LEVEL", "READ", "UNCOMITTED"),
 1657            ("LEVEL", "SERIALIZABLE"),
 1658        ),
 1659        "READ": ("WRITE", "ONLY"),
 1660    }
 1661
 1662    CONFLICT_ACTIONS: t.ClassVar[OPTIONS_TYPE] = {
 1663        **dict.fromkeys(("ABORT", "FAIL", "IGNORE", "REPLACE", "ROLLBACK", "UPDATE"), tuple()),
 1664        "DO": ("NOTHING", "UPDATE"),
 1665    }
 1666
 1667    TRIGGER_TIMING: t.ClassVar[OPTIONS_TYPE] = {
 1668        "INSTEAD": (("OF",),),
 1669        "BEFORE": tuple(),
 1670        "AFTER": tuple(),
 1671    }
 1672
 1673    TRIGGER_DEFERRABLE: t.ClassVar[OPTIONS_TYPE] = {
 1674        "NOT": (("DEFERRABLE",),),
 1675        "DEFERRABLE": tuple(),
 1676    }
 1677
 1678    CREATE_SEQUENCE: t.ClassVar[OPTIONS_TYPE] = {
 1679        "SCALE": ("EXTEND", "NOEXTEND"),
 1680        "SHARD": ("EXTEND", "NOEXTEND"),
 1681        "NO": ("CYCLE", "CACHE", "MAXVALUE", "MINVALUE"),
 1682        **dict.fromkeys(
 1683            (
 1684                "SESSION",
 1685                "GLOBAL",
 1686                "KEEP",
 1687                "NOKEEP",
 1688                "ORDER",
 1689                "NOORDER",
 1690                "NOCACHE",
 1691                "CYCLE",
 1692                "NOCYCLE",
 1693                "NOMINVALUE",
 1694                "NOMAXVALUE",
 1695                "NOSCALE",
 1696                "NOSHARD",
 1697            ),
 1698            tuple(),
 1699        ),
 1700    }
 1701
 1702    ISOLATED_LOADING_OPTIONS: t.ClassVar[OPTIONS_TYPE] = {"FOR": ("ALL", "INSERT", "NONE")}
 1703
 1704    USABLES: t.ClassVar[OPTIONS_TYPE] = dict.fromkeys(
 1705        ("ROLE", "WAREHOUSE", "DATABASE", "SCHEMA", "CATALOG"), tuple()
 1706    )
 1707
 1708    CAST_ACTIONS: t.ClassVar[OPTIONS_TYPE] = dict.fromkeys(("RENAME", "ADD"), ("FIELDS",))
 1709
 1710    SCHEMA_BINDING_OPTIONS: t.ClassVar[OPTIONS_TYPE] = {
 1711        "TYPE": ("EVOLUTION",),
 1712        **dict.fromkeys(("BINDING", "COMPENSATION", "EVOLUTION"), tuple()),
 1713    }
 1714
 1715    PROCEDURE_OPTIONS: t.ClassVar[OPTIONS_TYPE] = {}
 1716
 1717    EXECUTE_AS_OPTIONS: t.ClassVar[OPTIONS_TYPE] = dict.fromkeys(
 1718        ("CALLER", "SELF", "OWNER"), tuple()
 1719    )
 1720
 1721    KEY_CONSTRAINT_OPTIONS: t.ClassVar[OPTIONS_TYPE] = {
 1722        "NOT": ("ENFORCED",),
 1723        "MATCH": (
 1724            "FULL",
 1725            "PARTIAL",
 1726            "SIMPLE",
 1727        ),
 1728        "INITIALLY": ("DEFERRED", "IMMEDIATE"),
 1729        "USING": (
 1730            "BTREE",
 1731            "HASH",
 1732        ),
 1733        **dict.fromkeys(("DEFERRABLE", "NORELY", "RELY"), tuple()),
 1734    }
 1735
 1736    WINDOW_EXCLUDE_OPTIONS: t.ClassVar[OPTIONS_TYPE] = {
 1737        "NO": ("OTHERS",),
 1738        "CURRENT": ("ROW",),
 1739        **dict.fromkeys(("GROUP", "TIES"), tuple()),
 1740    }
 1741
 1742    INSERT_ALTERNATIVES: t.ClassVar = {"ABORT", "FAIL", "IGNORE", "REPLACE", "ROLLBACK"}
 1743
 1744    CLONE_KEYWORDS: t.ClassVar = {"CLONE", "COPY"}
 1745    # Time travel clause prefixes, mapped to whether they pin a timestamp or a version
 1746    VERSION_PHRASES: t.ClassVar[dict[tuple[str, ...], str]] = {
 1747        ("FOR", "SYSTEM_TIME"): "TIMESTAMP",
 1748        ("FOR", "SYSTEM", "TIME"): "TIMESTAMP",
 1749        ("FOR", "TIMESTAMP"): "TIMESTAMP",
 1750        ("FOR", "VERSION"): "VERSION",
 1751        ("TIMESTAMP", "AS", "OF"): "TIMESTAMP",
 1752        ("VERSION", "AS", "OF"): "VERSION",
 1753    }
 1754
 1755    HISTORICAL_DATA_PREFIX: t.ClassVar = {"AT", "BEFORE", "END"}
 1756    HISTORICAL_DATA_KIND: t.ClassVar = {"OFFSET", "STATEMENT", "STREAM", "TIMESTAMP", "VERSION"}
 1757
 1758    OPCLASS_FOLLOW_KEYWORDS: t.ClassVar = {"ASC", "DESC", "NULLS", "WITH"}
 1759
 1760    OPTYPE_FOLLOW_TOKENS: t.ClassVar = {TokenType.COMMA, TokenType.R_PAREN}
 1761
 1762    TABLE_INDEX_HINT_TOKENS: t.ClassVar = {TokenType.FORCE, TokenType.IGNORE, TokenType.USE}
 1763
 1764    VIEW_ATTRIBUTES: t.ClassVar = {"ENCRYPTION", "SCHEMABINDING", "VIEW_METADATA"}
 1765
 1766    WINDOW_ALIAS_TOKENS: t.ClassVar = ID_VAR_TOKENS - {TokenType.RANGE, TokenType.ROWS}
 1767    WINDOW_BEFORE_PAREN_TOKENS: t.ClassVar = {TokenType.OVER}
 1768    WINDOW_SIDES: t.ClassVar = {"FOLLOWING", "PRECEDING"}
 1769
 1770    JSON_KEY_VALUE_SEPARATOR_TOKENS: t.ClassVar = {TokenType.COLON, TokenType.COMMA, TokenType.IS}
 1771
 1772    FETCH_TOKENS: t.ClassVar = ID_VAR_TOKENS - {TokenType.ROW, TokenType.ROWS, TokenType.PERCENT}
 1773
 1774    ADD_CONSTRAINT_TOKENS: t.ClassVar = {
 1775        TokenType.CONSTRAINT,
 1776        TokenType.FOREIGN_KEY,
 1777        TokenType.INDEX,
 1778        TokenType.KEY,
 1779        TokenType.PRIMARY_KEY,
 1780        TokenType.UNIQUE,
 1781    }
 1782
 1783    # Constraint kinds that start an ALTER ... ADD action but aren't tokens of their own
 1784    ADD_CONSTRAINT_KEYWORDS: t.ClassVar[set[str]] = set()
 1785
 1786    DISTINCT_TOKENS: t.ClassVar = {TokenType.DISTINCT}
 1787
 1788    UNNEST_OFFSET_ALIAS_TOKENS: t.ClassVar = TABLE_ALIAS_TOKENS - SET_OPERATIONS
 1789
 1790    SELECT_START_TOKENS: t.ClassVar = {TokenType.L_PAREN, TokenType.WITH, TokenType.SELECT}
 1791
 1792    COPY_INTO_VARLEN_OPTIONS: t.ClassVar = {
 1793        "FILE_FORMAT",
 1794        "COPY_OPTIONS",
 1795        "FORMAT_OPTIONS",
 1796        "CREDENTIAL",
 1797    }
 1798
 1799    IS_JSON_PREDICATE_KIND: t.ClassVar = {"VALUE", "SCALAR", "ARRAY", "OBJECT"}
 1800
 1801    ODBC_DATETIME_LITERALS: t.ClassVar[dict[str, type[exp.Expr]]] = {}
 1802
 1803    ON_CONDITION_TOKENS: t.ClassVar = {"ERROR", "NULL", "TRUE", "FALSE", "EMPTY"}
 1804
 1805    PRIVILEGE_FOLLOW_TOKENS: t.ClassVar = {TokenType.ON, TokenType.COMMA, TokenType.L_PAREN}
 1806
 1807    # The style options for the DESCRIBE statement
 1808    DESCRIBE_STYLES: t.ClassVar = {"ANALYZE", "EXTENDED", "FORMATTED", "HISTORY"}
 1809
 1810    SET_ASSIGNMENT_DELIMITERS: t.ClassVar = {"=", ":=", "TO"}
 1811
 1812    # The style options for the ANALYZE statement
 1813    ANALYZE_STYLES: t.ClassVar = {
 1814        "BUFFER_USAGE_LIMIT",
 1815        "FULL",
 1816        "LOCAL",
 1817        "NO_WRITE_TO_BINLOG",
 1818        "SAMPLE",
 1819        "SKIP_LOCKED",
 1820        "VERBOSE",
 1821    }
 1822
 1823    ANALYZE_EXPRESSION_PARSERS: t.ClassVar = {
 1824        "ALL": lambda self: self._parse_analyze_columns(),
 1825        "COMPUTE": lambda self: self._parse_analyze_statistics(),
 1826        "DELETE": lambda self: self._parse_analyze_delete(),
 1827        "DROP": lambda self: self._parse_analyze_histogram(),
 1828        "ESTIMATE": lambda self: self._parse_analyze_statistics(),
 1829        "LIST": lambda self: self._parse_analyze_list(),
 1830        "PREDICATE": lambda self: self._parse_analyze_columns(),
 1831        "UPDATE": lambda self: self._parse_analyze_histogram(),
 1832        "VALIDATE": lambda self: self._parse_analyze_validate(),
 1833    }
 1834
 1835    PARTITION_KEYWORDS: t.ClassVar = {"PARTITION", "SUBPARTITION"}
 1836
 1837    AMBIGUOUS_ALIAS_TOKENS: t.ClassVar = (TokenType.LIMIT, TokenType.OFFSET)
 1838
 1839    OPERATION_MODIFIERS: t.ClassVar[set[str]] = set()
 1840
 1841    RECURSIVE_CTE_SEARCH_KIND: t.ClassVar = {"BREADTH", "DEPTH", "CYCLE"}
 1842
 1843    SECURITY_PROPERTY_KEYWORDS: t.ClassVar = {"DEFINER", "INVOKER", "NONE"}
 1844
 1845    MODIFIABLES: t.ClassVar = (exp.Query, exp.Table, exp.TableFromRows, exp.Values)
 1846
 1847    STRICT_CAST: t.ClassVar = True
 1848
 1849    PREFIXED_PIVOT_COLUMNS: t.ClassVar = False
 1850    IDENTIFY_PIVOT_STRINGS: t.ClassVar = False
 1851    # Whether an UNPIVOT outputs its value column(s) before the name column
 1852    UNPIVOT_VALUE_COLUMNS_FIRST: t.ClassVar = False
 1853    # Controls when an aggregation's name is included in a pivoted column's name:
 1854    # "agg_name_if_aliased" - only for aggregations that carry an explicit alias
 1855    # "agg_name_if_aliased_or_multiple" - if aliased, or whenever there are multiple aggregations
 1856    # "agg_name_if_multiple" - only when there are multiple aggregations (a lone agg is value-only)
 1857    PIVOT_COLUMN_NAMING: t.ClassVar[str] = "agg_name_if_aliased"
 1858
 1859    LOG_DEFAULTS_TO_LN: t.ClassVar = False
 1860
 1861    # Whether the table sample clause expects CSV syntax
 1862    TABLESAMPLE_CSV: t.ClassVar = False
 1863
 1864    # The default method used for table sampling
 1865    DEFAULT_SAMPLING_METHOD: t.ClassVar[str | None] = None
 1866
 1867    # Whether the SET command needs a delimiter (e.g. "=") for assignments
 1868    SET_REQUIRES_ASSIGNMENT_DELIMITER: t.ClassVar = True
 1869
 1870    # Whether the TRIM function expects the characters to trim as its first argument
 1871    TRIM_PATTERN_FIRST: t.ClassVar = False
 1872
 1873    # Whether string aliases are supported `SELECT COUNT(*) 'count'`
 1874    STRING_ALIASES: t.ClassVar = False
 1875
 1876    # Whether query modifiers such as LIMIT are attached to the UNION node (vs its right operand)
 1877    MODIFIERS_ATTACHED_TO_SET_OP: t.ClassVar = True
 1878    SET_OP_MODIFIERS: t.ClassVar = {"order", "limit", "offset", "sort", "distribute", "cluster"}
 1879
 1880    # Whether to parse IF statements that aren't followed by a left parenthesis as commands
 1881    NO_PAREN_IF_COMMANDS: t.ClassVar = True
 1882
 1883    # Whether the -> and ->> operators expect documents of type JSON (e.g. Postgres)
 1884    JSON_ARROWS_REQUIRE_JSON_TYPE: t.ClassVar = False
 1885
 1886    # Whether the `:` operator is used to extract a value from a VARIANT column
 1887    COLON_IS_VARIANT_EXTRACT: t.ClassVar = False
 1888
 1889    # Whether a chain of colon extractions (x:y:z) is a single extraction with a merged
 1890    # path (x:y.z, e.g. Snowflake) or each colon extracts from the previous result (e.g. Databricks)
 1891    COLON_CHAIN_IS_SINGLE_EXTRACT: t.ClassVar = True
 1892
 1893    # Whether or not a VALUES keyword needs to be followed by '(' to form a VALUES clause.
 1894    # If this is True and '(' is not found, the keyword will be treated as an identifier
 1895    VALUES_FOLLOWED_BY_PAREN: t.ClassVar = True
 1896
 1897    # Whether implicit unnesting is supported, e.g. SELECT 1 FROM y.z AS z, z.a (Redshift)
 1898    SUPPORTS_IMPLICIT_UNNEST: t.ClassVar = False
 1899
 1900    # Whether field names can be digit-prefixed, e.g. data.144A_FLAG or data.144 (BigQuery)
 1901    SUPPORTS_DIGIT_PREFIXED_FIELD_NAMES: t.ClassVar = False
 1902
 1903    # Whether or not interval spans are supported, INTERVAL 1 YEAR TO MONTHS
 1904    INTERVAL_SPANS: t.ClassVar = True
 1905
 1906    # Whether a PARTITION clause can follow a table reference
 1907    SUPPORTS_PARTITION_SELECTION: t.ClassVar = False
 1908
 1909    # Whether the `name AS expr` schema/column constraint requires parentheses around `expr`
 1910    WRAPPED_TRANSFORM_COLUMN_CONSTRAINT: t.ClassVar = True
 1911
 1912    # Whether the 'AS' keyword is optional in the CTE definition syntax
 1913    OPTIONAL_ALIAS_TOKEN_CTE: t.ClassVar = True
 1914
 1915    # Whether renaming a column with an ALTER statement requires the presence of the COLUMN keyword
 1916    ALTER_RENAME_REQUIRES_COLUMN: t.ClassVar = True
 1917
 1918    # Whether dropping a column with an ALTER statement requires the presence of the COLUMN keyword
 1919    ALTER_DROP_REQUIRES_COLUMN: t.ClassVar = True
 1920
 1921    # Whether Alter statements are allowed to contain Partition specifications
 1922    ALTER_TABLE_PARTITIONS: t.ClassVar = False
 1923
 1924    # Whether an ALTER can combine different actions, e.g. ADD COLUMN a INT, DROP COLUMN b
 1925    ALTER_TABLE_MIXED_ACTIONS: t.ClassVar = False
 1926
 1927    # Whether an ALTER TABLE needs an action, e.g. MySQL's ALTER TABLE t ENGINE=InnoDB has none
 1928    ALTER_TABLE_REQUIRES_ACTION: t.ClassVar = True
 1929
 1930    # Whether changing a column's type with ALTER COLUMN requires the TYPE keyword
 1931    ALTER_COLUMN_TYPE_REQUIRES_KEYWORD: t.ClassVar = False
 1932
 1933    # Whether all join types have the same precedence, i.e., they "naturally" produce a left-deep tree.
 1934    # In standard SQL, joins that use the JOIN keyword take higher precedence than comma-joins. That is
 1935    # to say, JOIN operators happen before comma operators. This is not the case in some dialects, such
 1936    # as BigQuery, where all joins have the same precedence.
 1937    JOINS_HAVE_EQUAL_PRECEDENCE: t.ClassVar = False
 1938
 1939    # Whether TIMESTAMP <literal> can produce a zone-aware timestamp
 1940    ZONE_AWARE_TIMESTAMP_CONSTRUCTOR: t.ClassVar = False
 1941
 1942    # Whether map literals support arbitrary expressions as keys.
 1943    # When True, allows complex keys like arrays or literals: {[1, 2]: 3}, {1: 2} (e.g. DuckDB).
 1944    # When False, keys are typically restricted to identifiers.
 1945    MAP_KEYS_ARE_ARBITRARY_EXPRESSIONS: t.ClassVar = False
 1946
 1947    # Whether JSON_EXTRACT requires a JSON expression as the first argument, e.g this
 1948    # is true for Snowflake but not for BigQuery which can also process strings
 1949    JSON_EXTRACT_REQUIRES_JSON_EXPRESSION: t.ClassVar = False
 1950
 1951    # Dialects like Databricks support JOINS without join criteria
 1952    # Adding an ON TRUE, makes transpilation semantically correct for other dialects
 1953    ADD_JOIN_ON_TRUE: t.ClassVar = False
 1954
 1955    # Whether INTERVAL spans with literal format '\d+ hh:[mm:[ss[.ff]]]'
 1956    # can omit the span unit `DAY TO MINUTE` or `DAY TO SECOND`
 1957    SUPPORTS_OMITTED_INTERVAL_SPAN_UNIT: t.ClassVar = False
 1958
 1959    # Whether adjacent string literals like 'foo' 'bar' require a whitespace or comment between them
 1960    # to be considered valid syntactically. Such expressions evaluate to the strings' concatenation.
 1961    ADJACENT_STRINGS_CANNOT_BE_CONNECTED: t.ClassVar = False
 1962
 1963    # Whether NTH_VALUE accepts the FROM FIRST | LAST modifier before its OVER clause,
 1964    # e.g. NTH_VALUE(x, 2) FROM LAST IGNORE NULLS OVER (...) (Oracle, Snowflake)
 1965    SUPPORTS_NTH_VALUE_FROM_MODIFIER: t.ClassVar = False
 1966
 1967    # Type names that denote a different type when they're quoted, so quoting has to be
 1968    # preserved instead of resolving them into the built-in type of the same name. These
 1969    # are matched case sensitively, e.g. PostgreSQL's one-byte "char" is not CHAR
 1970    QUOTED_TYPES_TO_PRESERVE: t.ClassVar[set[str]] = set()
 1971
 1972    SHOW_TRIE: t.ClassVar[dict] = new_trie(key.split(" ") for key in SHOW_PARSERS)
 1973    SET_TRIE: t.ClassVar[dict] = new_trie(key.split(" ") for key in SET_PARSERS)
 1974
 1975    def __init__(
 1976        self,
 1977        error_level: ErrorLevel | None = None,
 1978        error_message_context: int = 100,
 1979        max_errors: int = 3,
 1980        max_nodes: int = -1,
 1981        dialect: DialectType = None,
 1982    ):
 1983        self.error_level: ErrorLevel = error_level or ErrorLevel.IMMEDIATE
 1984        self.error_message_context: int = error_message_context
 1985        self.max_errors: int = max_errors
 1986        self.max_nodes: int = max_nodes
 1987        self.dialect: t.Any = _resolve_dialect(dialect)
 1988        self.sql: str = ""
 1989        self.errors: list[ParseError] = []
 1990        self._tokens: list[Token] = []
 1991        self._tokens_size: i64 = 0
 1992        self._index: i64 = 0
 1993        self._curr: Token = SENTINEL_NONE
 1994        self._next: Token = SENTINEL_NONE
 1995        self._prev: Token = SENTINEL_NONE
 1996        self._prev_comments: list[str] = []
 1997        self._pipe_cte_counter: int = 0
 1998        self._chunks: list[list[Token]] = []
 1999        self._chunk_index: i64 = 0
 2000        self._node_count: int = 0
 2001
 2002    def reset(self) -> None:
 2003        self.sql = ""
 2004        self.errors = []
 2005        self._tokens = []
 2006        self._tokens_size = 0
 2007        self._index = 0
 2008        self._curr = SENTINEL_NONE
 2009        self._next = SENTINEL_NONE
 2010        self._prev = SENTINEL_NONE
 2011        self._prev_comments = []
 2012        self._pipe_cte_counter = 0
 2013        self._chunks = []
 2014        self._chunk_index = 0
 2015        self._node_count = 0
 2016
 2017    def _advance(self, times: i64 = 1) -> None:
 2018        index = self._index + times
 2019        self._index = index
 2020        tokens = self._tokens
 2021        size = self._tokens_size
 2022        self._curr = tokens[index] if index < size else SENTINEL_NONE
 2023        self._next = tokens[index + 1] if index + 1 < size else SENTINEL_NONE
 2024
 2025        if index > 0:
 2026            prev = tokens[index - 1]
 2027            self._prev = prev
 2028            self._prev_comments = prev.comments
 2029        else:
 2030            self._prev = SENTINEL_NONE
 2031            self._prev_comments = []
 2032
 2033    def _advance_chunk(self) -> None:
 2034        self._index = -1
 2035        self._tokens = self._chunks[self._chunk_index]
 2036        self._tokens_size = i64(len(self._tokens))
 2037        self._chunk_index += 1
 2038        self._advance()
 2039
 2040    def _retreat(self, index: i64) -> None:
 2041        if index != self._index:
 2042            self._advance(index - self._index)
 2043
 2044    def _add_comments(self, expression: exp.Expr | None) -> None:
 2045        if expression and self._prev_comments:
 2046            expression.add_comments(self._prev_comments)
 2047            self._prev_comments = []
 2048
 2049    def _match(
 2050        self, token_type: TokenType, advance: bool = True, expression: exp.Expr | None = None
 2051    ) -> bool:
 2052        if self._curr.token_type == token_type:
 2053            if advance:
 2054                self._advance()
 2055            self._add_comments(expression)
 2056            return True
 2057        return False
 2058
 2059    def _match_set(self, types: t.Collection[TokenType], advance: bool = True) -> bool:
 2060        if self._curr.token_type in types:
 2061            if advance:
 2062                self._advance()
 2063            return True
 2064        return False
 2065
 2066    def _match_pair(
 2067        self, token_type_a: TokenType, token_type_b: TokenType, advance: bool = True
 2068    ) -> bool:
 2069        if self._curr.token_type == token_type_a and self._next.token_type == token_type_b:
 2070            if advance:
 2071                self._advance(2)
 2072            return True
 2073        return False
 2074
 2075    def _match_texts(self, texts: TEXTS_TYPE, advance: bool = True) -> bool:
 2076        if (
 2077            self._curr.token_type not in self.TEXT_MATCH_EXCLUDED_TOKENS
 2078            and self._curr.text.upper() in texts
 2079        ):
 2080            if advance:
 2081                self._advance()
 2082            return True
 2083        return False
 2084
 2085    def _match_text_seq(self, *texts: str, advance: bool = True) -> bool:
 2086        index = self._index
 2087        excluded_tokens = self.TEXT_MATCH_EXCLUDED_TOKENS
 2088        for text in texts:
 2089            if self._curr.token_type not in excluded_tokens and self._curr.text.upper() == text:
 2090                self._advance()
 2091            else:
 2092                self._retreat(index)
 2093                return False
 2094
 2095        if not advance:
 2096            self._retreat(index)
 2097
 2098        return True
 2099
 2100    def _is_connected(self) -> bool:
 2101        prev = self._prev
 2102        curr = self._curr
 2103        return bool(prev and curr and prev.end + 1 == curr.start)
 2104
 2105    def _find_sql(self, start: Token, end: Token) -> str:
 2106        return self.sql[start.start : end.end + 1]
 2107
 2108    def raise_error(self, message: str, token: Token = SENTINEL_NONE) -> None:
 2109        token = token or self._curr or self._prev or Token.string("")
 2110        formatted_sql, start_context, highlight, end_context = highlight_sql(
 2111            sql=self.sql,
 2112            positions=[(token.start, token.end)],
 2113            context_length=self.error_message_context,
 2114        )
 2115        formatted_message = f"{message}. Line {token.line}, Col: {token.col}.\n  {formatted_sql}"
 2116
 2117        error = ParseError.new(
 2118            formatted_message,
 2119            description=message,
 2120            line=token.line,
 2121            col=token.col,
 2122            start_context=start_context,
 2123            highlight=highlight,
 2124            end_context=end_context,
 2125        )
 2126
 2127        if self.error_level == ErrorLevel.IMMEDIATE:
 2128            raise error
 2129
 2130        self.errors.append(error)
 2131
 2132    def validate_expression(self, expression: E, args: list | None = None) -> E:
 2133        if self.max_nodes > -1:
 2134            self._node_count += 1
 2135            if self._node_count > self.max_nodes:
 2136                self.raise_error(f"Maximum number of AST nodes ({self.max_nodes}) exceeded")
 2137        if self.error_level != ErrorLevel.IGNORE:
 2138            for error_message in expression.error_messages(args):
 2139                self.raise_error(error_message)
 2140        return expression
 2141
 2142    def _try_parse(self, parse_method: t.Callable[[], T], retreat: bool = False) -> T | None:
 2143        index = self._index
 2144        error_level = self.error_level
 2145        this: T | None = None
 2146
 2147        self.error_level = ErrorLevel.IMMEDIATE
 2148        try:
 2149            this = parse_method()
 2150        except ParseError:
 2151            this = None
 2152        finally:
 2153            if not this or retreat:
 2154                self._retreat(index)
 2155            self.error_level = error_level
 2156
 2157        return this
 2158
 2159    def parse(self, raw_tokens: list[Token], sql: str) -> list[exp.Expr | None]:
 2160        """
 2161        Parses a list of tokens and returns a list of syntax trees, one tree
 2162        per parsed SQL statement.
 2163
 2164        Args:
 2165            raw_tokens: The list of tokens.
 2166            sql: The original SQL string.
 2167
 2168        Returns:
 2169            The list of the produced syntax trees.
 2170        """
 2171        return self._parse(
 2172            parse_method=self.__class__._parse_statement, raw_tokens=raw_tokens, sql=sql
 2173        )
 2174
 2175    def parse_into(
 2176        self,
 2177        expression_types: exp.IntoType,
 2178        raw_tokens: list[Token],
 2179        sql: str | None = None,
 2180    ) -> list[exp.Expr | None]:
 2181        """
 2182        Parses a list of tokens into a given Expr type. If a collection of Expr
 2183        types is given instead, this method will try to parse the token list into each one
 2184        of them, stopping at the first for which the parsing succeeds.
 2185
 2186        Args:
 2187            expression_types: The expression type(s) to try and parse the token list into.
 2188            raw_tokens: The list of tokens.
 2189            sql: The original SQL string, used to produce helpful debug messages.
 2190
 2191        Returns:
 2192            The target Expr.
 2193        """
 2194        errors = []
 2195        for expression_type in ensure_list(expression_types):
 2196            parser = self.EXPRESSION_PARSERS.get(t.cast(type[exp.Expr], expression_type))
 2197            if not parser:
 2198                raise TypeError(f"No parser registered for {expression_type}")
 2199
 2200            try:
 2201                return self._parse(parser, raw_tokens, sql)
 2202            except ParseError as e:
 2203                e.errors[0]["into_expression"] = expression_type
 2204                errors.append(e)
 2205
 2206        raise ParseError(
 2207            f"Failed to parse '{sql or raw_tokens}' into {expression_types}",
 2208            errors=merge_errors(errors),
 2209        ) from errors[-1]
 2210
 2211    def check_errors(self) -> None:
 2212        """Logs or raises any found errors, depending on the chosen error level setting."""
 2213        if self.error_level == ErrorLevel.WARN:
 2214            for error in self.errors:
 2215                logger.error(str(error))
 2216        elif self.error_level == ErrorLevel.RAISE and self.errors:
 2217            raise ParseError(
 2218                concat_messages(self.errors, self.max_errors),
 2219                errors=merge_errors(self.errors),
 2220            )
 2221
 2222    def expression(
 2223        self,
 2224        instance: E,
 2225        token: Token | None = None,
 2226        comments: list[str] | None = None,
 2227    ) -> E:
 2228        if token:
 2229            instance.update_positions(token)
 2230        instance.add_comments(comments) if comments else self._add_comments(instance)
 2231        if not instance.is_primitive:
 2232            instance = self.validate_expression(instance)
 2233        return instance
 2234
 2235    def _parse_batch_statements(
 2236        self,
 2237        parse_method: t.Callable[[Parser], exp.Expr | None],
 2238        sep_first_statement: bool = True,
 2239    ) -> list[exp.Expr | None]:
 2240        expressions = []
 2241
 2242        # Chunkification binds if/while statements with the first statement of the body
 2243        if sep_first_statement:
 2244            self._match(TokenType.BEGIN)
 2245            expressions.append(parse_method(self))
 2246
 2247        chunks_length = len(self._chunks)
 2248        while self._chunk_index < chunks_length:
 2249            self._advance_chunk()
 2250
 2251            if self._match(TokenType.ELSE, advance=False):
 2252                return expressions
 2253
 2254            if expressions and not self._next and self._match(TokenType.END):
 2255                expressions.append(exp.EndStatement())
 2256                continue
 2257
 2258            expressions.append(parse_method(self))
 2259
 2260            if self._index < self._tokens_size:
 2261                self.raise_error("Invalid expression / Unexpected token")
 2262
 2263            self.check_errors()
 2264
 2265        return expressions
 2266
 2267    def _parse(
 2268        self,
 2269        parse_method: t.Callable[[Parser], exp.Expr | None],
 2270        raw_tokens: list[Token],
 2271        sql: str | None = None,
 2272    ) -> list[exp.Expr | None]:
 2273        self.reset()
 2274        self.sql = sql or ""
 2275
 2276        total = len(raw_tokens)
 2277        chunks: list[list[Token]] = [[]]
 2278
 2279        for i, token in enumerate(raw_tokens):
 2280            if token.token_type == TokenType.SEMICOLON:
 2281                if token.comments:
 2282                    chunks.append([token])
 2283
 2284                if i < total - 1:
 2285                    chunks.append([])
 2286            else:
 2287                chunks[-1].append(token)
 2288
 2289        self._chunks = chunks
 2290
 2291        return self._parse_batch_statements(parse_method=parse_method, sep_first_statement=False)
 2292
 2293    def _warn_unsupported(self) -> None:
 2294        if self._tokens_size <= 1:
 2295            return
 2296
 2297        # We use _find_sql because self.sql may comprise multiple chunks, and we're only
 2298        # interested in emitting a warning for the one being currently processed.
 2299        sql = self._find_sql(self._tokens[0], self._tokens[-1])[: self.error_message_context]
 2300
 2301        logger.warning(
 2302            f"'{sql}' contains unsupported syntax. Falling back to parsing as a 'Command'."
 2303        )
 2304
 2305    def _parse_command(self) -> exp.Command:
 2306        self._warn_unsupported()
 2307        comments = self._prev_comments
 2308        return self.expression(
 2309            exp.Command(this=self._prev.text.upper(), expression=self._parse_string()),
 2310            comments=comments,
 2311        )
 2312
 2313    def _parse_comment(self, allow_exists: bool = True) -> exp.Expr:
 2314        start = self._prev
 2315        exists = self._parse_exists() if allow_exists else None
 2316
 2317        self._match(TokenType.ON)
 2318
 2319        materialized = self._match_text_seq("MATERIALIZED")
 2320        kind = self._match_set(self.CREATABLES) and self._prev
 2321        if not kind:
 2322            return self._parse_as_command(start)
 2323
 2324        if kind.token_type in (TokenType.FUNCTION, TokenType.PROCEDURE):
 2325            this = self._parse_user_defined_function(kind=kind.token_type)
 2326        elif kind.token_type == TokenType.TABLE:
 2327            this = self._parse_table(alias_tokens=self.COMMENT_TABLE_ALIAS_TOKENS)
 2328        elif kind.token_type == TokenType.COLUMN:
 2329            this = self._parse_column()
 2330        else:
 2331            this = self._parse_table_parts(schema=True)
 2332
 2333        self._match(TokenType.IS)
 2334
 2335        return self.expression(
 2336            exp.Comment(
 2337                this=this,
 2338                kind=kind.text,
 2339                expression=self._parse_string(),
 2340                exists=exists,
 2341                materialized=materialized,
 2342            )
 2343        )
 2344
 2345    def _parse_to_table(
 2346        self,
 2347    ) -> exp.ToTableProperty:
 2348        table = self._parse_table_parts(schema=True)
 2349        return self.expression(exp.ToTableProperty(this=table))
 2350
 2351    # https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/mergetree#mergetree-table-ttl
 2352    def _parse_ttl(self) -> exp.Expr:
 2353        def _parse_ttl_action() -> exp.Expr | None:
 2354            this = self._parse_bitwise()
 2355
 2356            if self._match_text_seq("DELETE"):
 2357                return self.expression(exp.MergeTreeTTLAction(this=this, delete=True))
 2358            if self._match_text_seq("RECOMPRESS"):
 2359                return self.expression(
 2360                    exp.MergeTreeTTLAction(this=this, recompress=self._parse_bitwise())
 2361                )
 2362            if self._match_text_seq("TO", "DISK"):
 2363                return self.expression(
 2364                    exp.MergeTreeTTLAction(this=this, to_disk=self._parse_string())
 2365                )
 2366            if self._match_text_seq("TO", "VOLUME"):
 2367                return self.expression(
 2368                    exp.MergeTreeTTLAction(this=this, to_volume=self._parse_string())
 2369                )
 2370
 2371            return this
 2372
 2373        expressions = self._parse_csv(_parse_ttl_action)
 2374        where = self._parse_where()
 2375        group = self._parse_group()
 2376
 2377        aggregates = None
 2378        if group and self._match(TokenType.SET):
 2379            aggregates = self._parse_csv(self._parse_set_item)
 2380
 2381        return self.expression(
 2382            exp.MergeTreeTTL(
 2383                expressions=expressions, where=where, group=group, aggregates=aggregates
 2384            )
 2385        )
 2386
 2387    def _parse_condition(self) -> exp.Expr | None:
 2388        return self._parse_wrapped(parse_method=self._parse_expression, optional=True)
 2389
 2390    def _parse_block(self) -> exp.Block:
 2391        return self.expression(
 2392            exp.Block(
 2393                expressions=self._parse_batch_statements(
 2394                    parse_method=lambda self: self._parse_statement()
 2395                )
 2396            )
 2397        )
 2398
 2399    def _parse_whileblock(self) -> exp.WhileBlock:
 2400        return self.expression(
 2401            exp.WhileBlock(this=self._parse_condition(), body=self._parse_block())
 2402        )
 2403
 2404    def _parse_statement(self) -> exp.Expr | None:
 2405        if not self._curr:
 2406            return None
 2407
 2408        if self._match_set(self.STATEMENT_PARSERS):
 2409            comments = self._prev_comments
 2410            stmt = self.STATEMENT_PARSERS[self._prev.token_type](self)
 2411            stmt.add_comments(comments, prepend=True)
 2412            return stmt
 2413
 2414        if self._match_set(self.dialect.tokenizer_class.COMMANDS):
 2415            return self._parse_command()
 2416
 2417        if self._match_text_seq("WHILE"):
 2418            return self._parse_whileblock()
 2419
 2420        expression = self._parse_expression()
 2421        expression = self._parse_set_operations(expression) if expression else self._parse_select()
 2422
 2423        if isinstance(expression, exp.Subquery) and self._match(TokenType.PIPE_GT, advance=False):
 2424            expression = self._parse_pipe_syntax_query(expression)
 2425
 2426        return self._parse_query_modifiers(expression)
 2427
 2428    def _parse_drop(self, exists: bool = False, kind: str | None = None) -> exp.Drop | exp.Command:
 2429        start = self._prev
 2430
 2431        if not kind:
 2432            temporary = self._match(TokenType.TEMPORARY)
 2433            materialized = self._match_text_seq("MATERIALIZED")
 2434            iceberg = self._match_text_seq("ICEBERG")
 2435            kind = self._prev.text.upper() if self._match_set(self.CREATABLES) else None
 2436        else:
 2437            temporary = False
 2438            materialized = False
 2439            iceberg = False
 2440
 2441        if not kind or (iceberg and kind != "TABLE"):
 2442            return self._parse_as_command(start)
 2443
 2444        concurrently = (
 2445            kind == "INDEX"
 2446            and self._next.token_type in self.ID_VAR_TOKENS
 2447            and self._match_text_seq("CONCURRENTLY")
 2448        )
 2449
 2450        if_exists = exists or self._parse_exists()
 2451
 2452        tables: exp.Expr | list[exp.Expr] | None
 2453        if kind == "COLUMN":
 2454            # A dropped column named after a keyword, like `any` or `charset`, is only a name
 2455            name = (
 2456                None
 2457                if self._curr.text.upper() in self.NO_PAREN_FUNCTION_PARSERS
 2458                else self._parse_column()
 2459            ) or self._parse_id_var()
 2460            tables = (
 2461                self.expression(exp.Column(this=name)) if isinstance(name, exp.Identifier) else name
 2462            )
 2463        elif kind in ("TABLE", "VIEW"):
 2464            tables = self._parse_csv(lambda: self._parse_table_parts(schema=True))
 2465        else:
 2466            tables = self._parse_table_parts(schema=True, is_db_reference=kind == "SCHEMA")
 2467
 2468        cluster: exp.Expr | None = None
 2469        if self._match(TokenType.ON):
 2470            if kind == "INDEX":
 2471                cluster = self.expression(exp.OnProperty(this=self._parse_table_parts()))
 2472            else:
 2473                cluster = self._parse_on_property()
 2474
 2475        if self._match(TokenType.L_PAREN, advance=False):
 2476            expressions = self._parse_wrapped_csv(self._parse_types)
 2477        else:
 2478            expressions = None
 2479
 2480        cascade_or_restrict = self._match_texts(("CASCADE", "RESTRICT")) and self._prev.text.upper()
 2481
 2482        return self.expression(
 2483            exp.Drop(
 2484                exists=if_exists,
 2485                tables=ensure_list(tables),
 2486                expressions=expressions,
 2487                kind=self.dialect.CREATABLE_KIND_MAPPING.get(kind) or kind,
 2488                temporary=temporary,
 2489                materialized=materialized,
 2490                cascade=cascade_or_restrict == "CASCADE",
 2491                restrict=cascade_or_restrict == "RESTRICT",
 2492                constraints=self._match_text_seq("CONSTRAINTS"),
 2493                purge=self._match_text_seq("PURGE"),
 2494                cluster=cluster,
 2495                concurrently=concurrently,
 2496                sync=self._match_text_seq("SYNC"),
 2497                iceberg=iceberg,
 2498                force=self._match_text_seq("FORCE"),
 2499            )
 2500        )
 2501
 2502    def _parse_exists(self, not_: bool = False) -> bool | None:
 2503        if not_:
 2504            return self._match_text_seq("IF", "NOT", "EXISTS")
 2505        return self._match_text_seq("IF", "EXISTS")
 2506
 2507    def _parse_create(self) -> exp.Create | exp.Command:
 2508        # Note: this can't be None because we've matched a statement parser
 2509        start = self._prev
 2510
 2511        replace = (
 2512            start.token_type == TokenType.REPLACE
 2513            or self._match_pair(TokenType.OR, TokenType.REPLACE)
 2514            or self._match_pair(TokenType.OR, TokenType.ALTER)
 2515        )
 2516        refresh = self._match_pair(TokenType.OR, TokenType.REFRESH)
 2517
 2518        unique = self._match(TokenType.UNIQUE)
 2519
 2520        if self._match_text_seq("CLUSTERED", "COLUMNSTORE"):
 2521            clustered = True
 2522        elif self._match_text_seq("NONCLUSTERED", "COLUMNSTORE") or self._match_text_seq(
 2523            "COLUMNSTORE"
 2524        ):
 2525            clustered = False
 2526        else:
 2527            clustered = None
 2528
 2529        if self._match_pair(TokenType.TABLE, TokenType.FUNCTION, advance=False):
 2530            self._advance()
 2531
 2532        properties = None
 2533        create_token = self._match_set(self.CREATABLES) and self._prev
 2534
 2535        if not create_token:
 2536            # exp.Properties.Location.POST_CREATE
 2537            properties = self._parse_properties()
 2538            create_token = self._match_set(self.CREATABLES) and self._prev
 2539
 2540            if not properties or not create_token:
 2541                return self._parse_as_command(start)
 2542
 2543        create_token_type = t.cast(Token, create_token).token_type
 2544
 2545        concurrently = self._match_text_seq("CONCURRENTLY")
 2546        exists = self._parse_exists(not_=True)
 2547        this = None
 2548        expression: exp.Expr | None = None
 2549        indexes = None
 2550        no_schema_binding = None
 2551        begin = None
 2552        clone = None
 2553
 2554        def extend_props(temp_props: exp.Properties | None) -> None:
 2555            nonlocal properties
 2556            if properties and temp_props:
 2557                properties.expressions.extend(temp_props.expressions)
 2558            elif temp_props:
 2559                properties = temp_props
 2560
 2561        if create_token_type in (TokenType.FUNCTION, TokenType.PROCEDURE):
 2562            this = self._parse_user_defined_function(kind=create_token_type)
 2563
 2564            # exp.Properties.Location.POST_SCHEMA ("schema" here is the UDF's type signature)
 2565            extend_props(self._parse_properties())
 2566
 2567            expression = self._parse_heredoc() if self._match(TokenType.ALIAS) else None
 2568
 2569            if (
 2570                not expression
 2571                and create_token_type == TokenType.FUNCTION
 2572                and isinstance(this, exp.UserDefinedFunction)
 2573                and this.args.get("wrapped")
 2574            ):
 2575                pre_table_index = self._index
 2576                is_table = self._match(TokenType.TABLE)
 2577
 2578                expression = self._parse_expression()
 2579                overload_mode = bool(
 2580                    expression
 2581                    and self._curr.token_type == TokenType.COMMA
 2582                    and self._next.token_type == TokenType.L_PAREN
 2583                )
 2584                if not overload_mode:
 2585                    self._retreat(pre_table_index)
 2586                    is_table = False
 2587                    expression = None
 2588            else:
 2589                is_table = False
 2590                overload_mode = False
 2591
 2592            extend_props(self._parse_function_properties())
 2593
 2594            if not expression:
 2595                if self._match(TokenType.COMMAND):
 2596                    expression = self._parse_as_command(self._prev)
 2597                else:
 2598                    begin = self._match(TokenType.BEGIN)
 2599                    return_ = self._match_text_seq("RETURN")
 2600
 2601                    if self._match(TokenType.STRING, advance=False):
 2602                        # Takes care of BigQuery's JavaScript UDF definitions that end in an OPTIONS property
 2603                        # # https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_function_statement
 2604                        expression = self._parse_string()
 2605                        extend_props(self._parse_properties())
 2606                    else:
 2607                        expression = (
 2608                            self._parse_user_defined_function_expression()
 2609                            if create_token_type == TokenType.FUNCTION
 2610                            else self._parse_block()
 2611                        )
 2612
 2613                    if return_:
 2614                        expression = self.expression(exp.Return(this=expression))
 2615
 2616            if overload_mode and expression:
 2617                expression = self._parse_macro_overloads(
 2618                    t.cast(exp.UserDefinedFunction, this), expression, is_table
 2619                )
 2620        elif create_token_type == TokenType.INDEX:
 2621            # Postgres allows anonymous indexes, eg. CREATE INDEX IF NOT EXISTS ON t(c)
 2622            if not self._match(TokenType.ON):
 2623                index = self._parse_id_var()
 2624                anonymous = False
 2625            else:
 2626                index = None
 2627                anonymous = True
 2628
 2629            this = self._parse_index(index=index, anonymous=anonymous)
 2630        elif (
 2631            create_token_type == TokenType.CONSTRAINT and self._match(TokenType.TRIGGER)
 2632        ) or create_token_type == TokenType.TRIGGER:
 2633            if is_constraint := (create_token_type == TokenType.CONSTRAINT):
 2634                create_token = self._prev
 2635
 2636            trigger_name = self._parse_id_var()
 2637            if not trigger_name:
 2638                return self._parse_as_command(start)
 2639
 2640            timing_var = self._parse_var_from_options(self.TRIGGER_TIMING, raise_unmatched=False)
 2641            timing = timing_var.this if timing_var else None
 2642            if not timing:
 2643                return self._parse_as_command(start)
 2644
 2645            events = self._parse_trigger_events()
 2646            if not self._match(TokenType.ON):
 2647                self.raise_error("Expected ON in trigger definition")
 2648
 2649            table = self._parse_table_parts()
 2650            referenced_table = self._parse_table_parts() if self._match(TokenType.FROM) else None
 2651            deferrable, initially = self._parse_trigger_deferrable()
 2652            referencing = self._parse_trigger_referencing()
 2653            for_each = self._parse_trigger_for_each()
 2654            when = self._match_text_seq("WHEN") and self._parse_wrapped(
 2655                self._parse_disjunction, optional=True
 2656            )
 2657            execute = self._parse_trigger_execute()
 2658
 2659            if execute is None:
 2660                return self._parse_as_command(start)
 2661
 2662            trigger_props = self.expression(
 2663                exp.TriggerProperties(
 2664                    table=table,
 2665                    timing=timing,
 2666                    events=events,
 2667                    execute=execute,
 2668                    constraint=is_constraint,
 2669                    referenced_table=referenced_table,
 2670                    deferrable=deferrable,
 2671                    initially=initially,
 2672                    referencing=referencing,
 2673                    for_each=for_each,
 2674                    when=when,
 2675                )
 2676            )
 2677
 2678            this = trigger_name
 2679            extend_props(exp.Properties(expressions=[trigger_props] if trigger_props else []))
 2680        elif create_token_type == TokenType.TYPE:
 2681            this = self._parse_table_parts(schema=True)
 2682            if not this or not self._match(TokenType.ALIAS):
 2683                return self._parse_as_command(start)
 2684
 2685            if self._match(TokenType.ENUM):
 2686                expression = exp.DataType(
 2687                    this=exp.DType.ENUM,
 2688                    expressions=self._parse_wrapped_csv(self._parse_string),
 2689                )
 2690            elif self._match(TokenType.L_PAREN, advance=False):
 2691                expression = self._parse_schema()
 2692            else:
 2693                return self._parse_as_command(start)
 2694        elif create_token_type in self.DB_CREATABLES:
 2695            table_parts = self._parse_table_parts(
 2696                schema=True, is_db_reference=create_token_type == TokenType.SCHEMA
 2697            )
 2698
 2699            # exp.Properties.Location.POST_NAME
 2700            self._match(TokenType.COMMA)
 2701            extend_props(self._parse_properties(before=True))
 2702
 2703            this = self._parse_schema(this=table_parts)
 2704
 2705            # exp.Properties.Location.POST_SCHEMA and POST_WITH
 2706            extend_props(self._parse_properties())
 2707
 2708            has_alias = self._match(TokenType.ALIAS)
 2709            if not self._match_set(self.DDL_SELECT_TOKENS, advance=False):
 2710                # exp.Properties.Location.POST_ALIAS
 2711                extend_props(self._parse_properties())
 2712
 2713            if create_token_type == TokenType.SEQUENCE:
 2714                expression = self._parse_types()
 2715                props = self._parse_properties()
 2716                if props:
 2717                    sequence_props = exp.SequenceProperties()
 2718                    options = []
 2719                    for prop in props:
 2720                        if isinstance(prop, exp.SequenceProperties):
 2721                            for arg, value in prop.args.items():
 2722                                if arg == "options":
 2723                                    options.extend(value)
 2724                                else:
 2725                                    sequence_props.set(arg, value)
 2726                            prop.pop()
 2727
 2728                    if options:
 2729                        sequence_props.set("options", options)
 2730
 2731                    props.append("expressions", sequence_props)
 2732                    extend_props(props)
 2733            else:
 2734                expression = self._parse_ddl_select()
 2735
 2736                # Some dialects also support using a table as an alias instead of a SELECT.
 2737                # Here we fallback to this as an alternative.
 2738                if not expression and has_alias:
 2739                    expression = self._try_parse(self._parse_table_parts)
 2740
 2741            if create_token_type == TokenType.TABLE:
 2742                # exp.Properties.Location.POST_EXPRESSION
 2743                extend_props(self._parse_properties())
 2744
 2745                indexes = []
 2746                while True:
 2747                    index = self._parse_index()
 2748
 2749                    # exp.Properties.Location.POST_INDEX
 2750                    extend_props(self._parse_properties())
 2751                    if not index:
 2752                        break
 2753                    else:
 2754                        self._match(TokenType.COMMA)
 2755                        indexes.append(index)
 2756            elif create_token_type == TokenType.VIEW:
 2757                if self._match_text_seq("WITH", "NO", "SCHEMA", "BINDING"):
 2758                    no_schema_binding = True
 2759            elif create_token_type in (TokenType.SINK, TokenType.SOURCE):
 2760                extend_props(self._parse_properties())
 2761
 2762            shallow = self._match_text_seq("SHALLOW")
 2763
 2764            if self._match_texts(self.CLONE_KEYWORDS):
 2765                copy = self._prev.text.lower() == "copy"
 2766                clone = self.expression(
 2767                    exp.Clone(this=self._parse_table(schema=True), shallow=shallow, copy=copy)
 2768                )
 2769
 2770        if self._curr and not self._match_set((TokenType.R_PAREN, TokenType.COMMA), advance=False):
 2771            return self._parse_as_command(start)
 2772
 2773        create_kind_text = create_token.text.upper()
 2774        return self.expression(
 2775            exp.Create(
 2776                this=this,
 2777                kind=self.dialect.CREATABLE_KIND_MAPPING.get(create_kind_text) or create_kind_text,
 2778                replace=replace,
 2779                refresh=refresh,
 2780                unique=unique,
 2781                expression=expression,
 2782                exists=exists,
 2783                properties=properties,
 2784                indexes=indexes,
 2785                no_schema_binding=no_schema_binding,
 2786                begin=begin,
 2787                clone=clone,
 2788                concurrently=concurrently,
 2789                clustered=clustered,
 2790            )
 2791        )
 2792
 2793    def _parse_sequence_properties(self) -> exp.SequenceProperties | None:
 2794        seq = exp.SequenceProperties()
 2795
 2796        options = []
 2797        index = self._index
 2798
 2799        while self._curr:
 2800            self._match(TokenType.COMMA)
 2801            if self._match_text_seq("INCREMENT"):
 2802                self._match_text_seq("BY")
 2803                self._match_text_seq("=")
 2804                seq.set("increment", self._parse_term())
 2805            elif self._match_text_seq("MINVALUE"):
 2806                seq.set("minvalue", self._parse_term())
 2807            elif self._match_text_seq("MAXVALUE"):
 2808                seq.set("maxvalue", self._parse_term())
 2809            elif self._match_text_seq("START"):
 2810                self._match_text_seq("WITH")
 2811                self._match_text_seq("=")
 2812                seq.set("start", self._parse_term())
 2813            elif self._match_text_seq("CACHE"):
 2814                # T-SQL allows empty CACHE which is initialized dynamically
 2815                seq.set("cache", self._parse_number() or True)
 2816            elif self._match_text_seq("OWNED", "BY"):
 2817                # "OWNED BY NONE" is the default
 2818                seq.set("owned", None if self._match_text_seq("NONE") else self._parse_column())
 2819            else:
 2820                opt = self._parse_var_from_options(self.CREATE_SEQUENCE, raise_unmatched=False)
 2821                if opt:
 2822                    options.append(opt)
 2823                else:
 2824                    break
 2825
 2826        seq.set("options", options if options else None)
 2827        return None if self._index == index else seq
 2828
 2829    def _parse_trigger_events(self) -> list[exp.TriggerEvent]:
 2830        events = []
 2831
 2832        while True:
 2833            event_type = self._match_set(self.TRIGGER_EVENTS) and self._prev.text.upper()
 2834
 2835            if not event_type:
 2836                self.raise_error("Expected trigger event (INSERT, UPDATE, DELETE, TRUNCATE)")
 2837
 2838            columns = (
 2839                self._parse_csv(self._parse_column)
 2840                if event_type == "UPDATE" and self._match_text_seq("OF")
 2841                else None
 2842            )
 2843
 2844            events.append(self.expression(exp.TriggerEvent(this=event_type, columns=columns)))
 2845
 2846            if not self._match(TokenType.OR):
 2847                break
 2848
 2849        return events
 2850
 2851    def _parse_trigger_deferrable(
 2852        self,
 2853    ) -> tuple[str | None, str | None]:
 2854        deferrable_var = self._parse_var_from_options(
 2855            self.TRIGGER_DEFERRABLE, raise_unmatched=False
 2856        )
 2857        deferrable = deferrable_var.this if deferrable_var else None
 2858
 2859        initially = None
 2860        if deferrable and self._match_text_seq("INITIALLY"):
 2861            initially = (
 2862                self._prev.text.upper() if self._match_texts(("IMMEDIATE", "DEFERRED")) else None
 2863            )
 2864
 2865        return deferrable, initially
 2866
 2867    def _parse_trigger_referencing_clause(self, keyword: str) -> exp.Expr | None:
 2868        if not self._match_text_seq(keyword):
 2869            return None
 2870        if not self._match_text_seq("TABLE"):
 2871            self.raise_error(f"Expected TABLE after {keyword} in REFERENCING clause")
 2872        self._match_text_seq("AS")
 2873        return self._parse_id_var()
 2874
 2875    def _parse_trigger_referencing(self) -> exp.TriggerReferencing | None:
 2876        if not self._match_text_seq("REFERENCING"):
 2877            return None
 2878
 2879        old_alias = None
 2880        new_alias = None
 2881
 2882        while True:
 2883            if alias := self._parse_trigger_referencing_clause("OLD"):
 2884                if old_alias is not None:
 2885                    self.raise_error("Duplicate OLD clause in REFERENCING")
 2886                old_alias = alias
 2887            elif alias := self._parse_trigger_referencing_clause("NEW"):
 2888                if new_alias is not None:
 2889                    self.raise_error("Duplicate NEW clause in REFERENCING")
 2890                new_alias = alias
 2891            else:
 2892                break
 2893
 2894        if old_alias is None and new_alias is None:
 2895            self.raise_error("REFERENCING clause requires at least OLD TABLE or NEW TABLE")
 2896
 2897        return self.expression(exp.TriggerReferencing(old=old_alias, new=new_alias))
 2898
 2899    def _parse_trigger_for_each(self) -> str | None:
 2900        if not self._match_text_seq("FOR", "EACH"):
 2901            return None
 2902
 2903        return self._prev.text.upper() if self._match_texts(("ROW", "STATEMENT")) else None
 2904
 2905    def _parse_trigger_execute(self) -> exp.TriggerExecute | None:
 2906        if not self._match(TokenType.EXECUTE):
 2907            return None
 2908
 2909        if not self._match_set((TokenType.FUNCTION, TokenType.PROCEDURE)):
 2910            self.raise_error("Expected FUNCTION or PROCEDURE after EXECUTE")
 2911
 2912        func_call = self._parse_column()
 2913        return self.expression(exp.TriggerExecute(this=func_call))
 2914
 2915    def _parse_property_before(self) -> exp.Expr | list[exp.Expr] | None:
 2916        # only used for teradata currently
 2917        self._match(TokenType.COMMA)
 2918
 2919        kwargs = {
 2920            "no": self._match_text_seq("NO"),
 2921            "dual": self._match_text_seq("DUAL"),
 2922            "before": self._match_text_seq("BEFORE"),
 2923            "default": self._match_text_seq("DEFAULT"),
 2924            "local": (self._match_text_seq("LOCAL") and "LOCAL")
 2925            or (self._match_text_seq("NOT", "LOCAL") and "NOT LOCAL"),
 2926            "after": self._match_text_seq("AFTER"),
 2927            "minimum": self._match_texts(("MIN", "MINIMUM")),
 2928            "maximum": self._match_texts(("MAX", "MAXIMUM")),
 2929        }
 2930
 2931        if self._match_texts(self.PROPERTY_PARSERS):
 2932            parser = self.PROPERTY_PARSERS[self._prev.text.upper()]
 2933            try:
 2934                return parser(self, **{k: v for k, v in kwargs.items() if v})
 2935            except TypeError:
 2936                self.raise_error(f"Cannot parse property '{self._prev.text}'")
 2937
 2938        if self._match_pair(TokenType.CHAR, TokenType.SET):
 2939            return self._parse_character_set(default=bool(kwargs["default"]))
 2940
 2941        return None
 2942
 2943    def _parse_wrapped_properties(self) -> list[exp.Expr | list[exp.Expr]]:
 2944        return self._parse_wrapped_csv(self._parse_property)
 2945
 2946    def _parse_property(self) -> exp.Expr | list[exp.Expr] | None:
 2947        if self._match_texts(self.PROPERTY_PARSERS):
 2948            return self.PROPERTY_PARSERS[self._prev.text.upper()](self)
 2949
 2950        if self._match_pair(TokenType.CHAR, TokenType.SET):
 2951            return self._parse_character_set()
 2952
 2953        if self._match(TokenType.DEFAULT):
 2954            if self._match_texts(self.PROPERTY_PARSERS):
 2955                return self.PROPERTY_PARSERS[self._prev.text.upper()](self, default=True)
 2956
 2957            if self._match_pair(TokenType.CHAR, TokenType.SET):
 2958                return self._parse_character_set(default=True)
 2959
 2960        if self._match_text_seq("COMPOUND", "SORTKEY"):
 2961            return self._parse_sortkey(compound=True)
 2962
 2963        if self._match_text_seq("PARAMETER", "STYLE", "PANDAS"):
 2964            return self.expression(exp.ParameterStyleProperty(this="PANDAS"))
 2965
 2966        if self._match_text_seq("NOT", "DETERMINISTIC"):
 2967            return self.expression(exp.StabilityProperty(this=exp.Literal.string("VOLATILE")))
 2968
 2969        index = self._index
 2970
 2971        seq_props = self._parse_sequence_properties()
 2972        if seq_props:
 2973            return seq_props
 2974
 2975        self._retreat(index)
 2976        return self._parse_key_value_property()
 2977
 2978    def _parse_key_value_property(
 2979        self, parse_value: t.Callable[[], exp.Expr | None] | None = None
 2980    ) -> exp.Property | None:
 2981        index = self._index
 2982        key = self._parse_column()
 2983
 2984        if not self._match(TokenType.EQ):
 2985            self._retreat(index)
 2986            return None
 2987
 2988        # Transform the key to exp.Dot if it's dotted identifiers wrapped in exp.Column or to exp.Var otherwise
 2989        if isinstance(key, exp.Column):
 2990            key = key.to_dot() if len(key.parts) > 1 else exp.var(key.name)
 2991
 2992        value = (
 2993            parse_value()
 2994            if parse_value
 2995            else self._parse_bitwise() or self._parse_var(any_token=True)
 2996        )
 2997
 2998        # Transform the value to exp.Var if it was parsed as exp.Column(exp.Identifier())
 2999        if isinstance(value, exp.Column):
 3000            value = exp.var(value.name)
 3001
 3002        return self.expression(exp.Property(this=key, value=value))
 3003
 3004    def _parse_stored(self) -> exp.FileFormatProperty | exp.StorageHandlerProperty:
 3005        if self._match_text_seq("BY"):
 3006            return self.expression(exp.StorageHandlerProperty(this=self._parse_var_or_string()))
 3007
 3008        self._match(TokenType.ALIAS)
 3009        input_format = self._parse_string() if self._match_text_seq("INPUTFORMAT") else None
 3010        output_format = self._parse_string() if self._match_text_seq("OUTPUTFORMAT") else None
 3011
 3012        return self.expression(
 3013            exp.FileFormatProperty(
 3014                this=(
 3015                    self.expression(
 3016                        exp.InputOutputFormat(
 3017                            input_format=input_format, output_format=output_format
 3018                        )
 3019                    )
 3020                    if input_format or output_format
 3021                    else self._parse_var_or_string() or self._parse_number() or self._parse_id_var()
 3022                ),
 3023                hive_format=True,
 3024            )
 3025        )
 3026
 3027    def _parse_unquoted_field(self) -> exp.Expr | None:
 3028        field = self._parse_field()
 3029        if isinstance(field, exp.Identifier) and not field.quoted:
 3030            field = exp.var(field)
 3031
 3032        return field
 3033
 3034    def _parse_property_assignment(self, exp_class: type[E], **kwargs: t.Any) -> E:
 3035        self._match(TokenType.EQ)
 3036        self._match(TokenType.ALIAS)
 3037
 3038        return self.expression(exp_class(this=self._parse_unquoted_field(), **kwargs))
 3039
 3040    def _parse_properties(self, before: bool | None = None) -> exp.Properties | None:
 3041        properties = []
 3042        while True:
 3043            if before:
 3044                prop = self._parse_property_before()
 3045            else:
 3046                prop = self._parse_property()
 3047            if not prop:
 3048                break
 3049            for p in ensure_list(prop):
 3050                properties.append(p)
 3051
 3052            self._match(TokenType.COMMA, expression=properties[-1])
 3053
 3054        if properties:
 3055            return self.expression(exp.Properties(expressions=properties))
 3056
 3057        return None
 3058
 3059    def _parse_fallback(self, no: bool = False) -> exp.FallbackProperty:
 3060        return self.expression(
 3061            exp.FallbackProperty(no=no, protection=self._match_text_seq("PROTECTION"))
 3062        )
 3063
 3064    def _parse_sql_security(self) -> exp.SqlSecurityProperty:
 3065        return self.expression(
 3066            exp.SqlSecurityProperty(
 3067                this=self._match_texts(self.SECURITY_PROPERTY_KEYWORDS) and self._prev.text.upper()
 3068            )
 3069        )
 3070
 3071    def _parse_settings_property(self) -> exp.SettingsProperty:
 3072        return self.expression(
 3073            exp.SettingsProperty(expressions=self._parse_csv(self._parse_assignment))
 3074        )
 3075
 3076    def _parse_called_on_null_input_property(self) -> exp.CalledOnNullInputProperty | None:
 3077        if not self._match_text_seq("ON", "NULL", "INPUT"):
 3078            self._retreat(self._index - 1)
 3079            return None
 3080
 3081        return self.expression(exp.CalledOnNullInputProperty())
 3082
 3083    def _parse_volatile_property(self) -> exp.VolatileProperty | exp.StabilityProperty:
 3084        if self._index >= 2:
 3085            pre_volatile_token = self._tokens[self._index - 2]
 3086        else:
 3087            pre_volatile_token = None
 3088
 3089        if pre_volatile_token and pre_volatile_token.token_type in self.PRE_VOLATILE_TOKENS:
 3090            return exp.VolatileProperty()
 3091
 3092        return self.expression(exp.StabilityProperty(this=exp.Literal.string("VOLATILE")))
 3093
 3094    def _parse_retention_period(self) -> exp.Var:
 3095        # Parse TSQL's HISTORY_RETENTION_PERIOD: {INFINITE | <number> DAY | DAYS | MONTH ...}
 3096        number = self._parse_number()
 3097        number_str = f"{number} " if number else ""
 3098        unit = self._parse_var(any_token=True)
 3099        return exp.var(f"{number_str}{unit}")
 3100
 3101    def _parse_system_versioning_property(
 3102        self, with_: bool = False
 3103    ) -> exp.WithSystemVersioningProperty:
 3104        self._match(TokenType.EQ)
 3105        prop = self.expression(exp.WithSystemVersioningProperty(on=True, with_=with_))
 3106
 3107        if self._match_text_seq("OFF"):
 3108            prop.set("on", False)
 3109            return prop
 3110
 3111        self._match(TokenType.ON)
 3112        if self._match(TokenType.L_PAREN):
 3113            while self._curr and not self._match(TokenType.R_PAREN):
 3114                if self._match_text_seq("HISTORY_TABLE", "="):
 3115                    prop.set("this", self._parse_table_parts())
 3116                elif self._match_text_seq("DATA_CONSISTENCY_CHECK", "="):
 3117                    prop.set("data_consistency", self._advance_any() and self._prev.text.upper())
 3118                elif self._match_text_seq("HISTORY_RETENTION_PERIOD", "="):
 3119                    prop.set("retention_period", self._parse_retention_period())
 3120
 3121                self._match(TokenType.COMMA)
 3122
 3123        return prop
 3124
 3125    def _parse_data_deletion_property(self) -> exp.DataDeletionProperty:
 3126        self._match(TokenType.EQ)
 3127        on = self._match_text_seq("ON") or not self._match_text_seq("OFF")
 3128        prop = self.expression(exp.DataDeletionProperty(on=on))
 3129
 3130        if self._match(TokenType.L_PAREN):
 3131            while self._curr and not self._match(TokenType.R_PAREN):
 3132                if self._match_text_seq("FILTER_COLUMN", "="):
 3133                    prop.set("filter_column", self._parse_column())
 3134                elif self._match_text_seq("RETENTION_PERIOD", "="):
 3135                    prop.set("retention_period", self._parse_retention_period())
 3136
 3137                self._match(TokenType.COMMA)
 3138
 3139        return prop
 3140
 3141    def _parse_distributed_property(self) -> exp.DistributedByProperty:
 3142        kind = "HASH"
 3143        expressions: list[exp.Expr] | None = None
 3144        if self._match_text_seq("BY", "HASH"):
 3145            expressions = self._parse_wrapped_csv(self._parse_id_var)
 3146        elif self._match_text_seq("BY", "RANDOM"):
 3147            kind = "RANDOM"
 3148
 3149        # If the BUCKETS keyword is not present, the number of buckets is AUTO
 3150        buckets: exp.Expr | None = None
 3151        if self._match_text_seq("BUCKETS") and not self._match_text_seq("AUTO"):
 3152            buckets = self._parse_number()
 3153
 3154        return self.expression(
 3155            exp.DistributedByProperty(
 3156                expressions=expressions, kind=kind, buckets=buckets, order=self._parse_order()
 3157            )
 3158        )
 3159
 3160    def _parse_composite_key_property(self, expr_type: type[E]) -> E:
 3161        self._match_text_seq("KEY")
 3162        expressions = self._parse_wrapped_id_vars()
 3163        return self.expression(expr_type(expressions=expressions))
 3164
 3165    def _parse_with_property(self) -> exp.Expr | None | list[exp.Expr]:
 3166        if self._match_text_seq("(", "SYSTEM_VERSIONING"):
 3167            prop = self._parse_system_versioning_property(with_=True)
 3168            self._match_r_paren()
 3169            return prop
 3170
 3171        if self._match(TokenType.L_PAREN, advance=False):
 3172            result: list[exp.Expr] = []
 3173            for i in self._parse_wrapped_properties():
 3174                result.extend(i) if isinstance(i, list) else result.append(i)
 3175            return result
 3176
 3177        if self._match_text_seq("JOURNAL"):
 3178            return self._parse_withjournaltable()
 3179
 3180        if self._match_texts(self.VIEW_ATTRIBUTES):
 3181            return self.expression(exp.ViewAttributeProperty(this=self._prev.text.upper()))
 3182
 3183        if self._match_text_seq("DATA"):
 3184            return self._parse_withdata(no=False)
 3185        elif self._match_text_seq("NO", "DATA"):
 3186            return self._parse_withdata(no=True)
 3187
 3188        if self._match(TokenType.SERDE_PROPERTIES, advance=False):
 3189            return self._parse_serde_properties(with_=True)
 3190
 3191        if self._match(TokenType.SCHEMA):
 3192            return self.expression(
 3193                exp.WithSchemaBindingProperty(
 3194                    this=self._parse_var_from_options(self.SCHEMA_BINDING_OPTIONS)
 3195                )
 3196            )
 3197
 3198        if self._match_texts(self.PROCEDURE_OPTIONS, advance=False):
 3199            return self.expression(
 3200                exp.WithProcedureOptions(expressions=self._parse_csv(self._parse_procedure_option))
 3201            )
 3202
 3203        if not self._next:
 3204            return None
 3205
 3206        return self._parse_withisolatedloading()
 3207
 3208    def _parse_procedure_option(self) -> exp.Expr | None:
 3209        if self._match_text_seq("EXECUTE", "AS"):
 3210            return self.expression(
 3211                exp.ExecuteAsProperty(
 3212                    this=self._parse_var_from_options(
 3213                        self.EXECUTE_AS_OPTIONS, raise_unmatched=False
 3214                    )
 3215                    or self._parse_string()
 3216                )
 3217            )
 3218
 3219        return self._parse_var_from_options(self.PROCEDURE_OPTIONS)
 3220
 3221    # https://dev.mysql.com/doc/refman/8.0/en/create-view.html
 3222    def _parse_definer(self) -> exp.DefinerProperty | None:
 3223        self._match(TokenType.EQ)
 3224
 3225        user = self._parse_id_var()
 3226        self._match(TokenType.PARAMETER)
 3227        host = self._parse_id_var() or (self._match(TokenType.MOD) and self._prev.text)
 3228
 3229        if not user or not host:
 3230            return None
 3231
 3232        return exp.DefinerProperty(this=f"{user}@{host}")
 3233
 3234    def _parse_withjournaltable(self) -> exp.WithJournalTableProperty:
 3235        self._match(TokenType.TABLE)
 3236        self._match(TokenType.EQ)
 3237        return self.expression(exp.WithJournalTableProperty(this=self._parse_table_parts()))
 3238
 3239    def _parse_log(self, no: bool = False) -> exp.LogProperty:
 3240        return self.expression(exp.LogProperty(no=no))
 3241
 3242    def _parse_journal(self, **kwargs) -> exp.JournalProperty:
 3243        return self.expression(exp.JournalProperty(**kwargs))
 3244
 3245    def _parse_checksum(self) -> exp.ChecksumProperty:
 3246        self._match(TokenType.EQ)
 3247
 3248        on = None
 3249        if self._match(TokenType.ON):
 3250            on = True
 3251        elif self._match_text_seq("OFF"):
 3252            on = False
 3253
 3254        return self.expression(exp.ChecksumProperty(on=on, default=self._match(TokenType.DEFAULT)))
 3255
 3256    def _parse_cluster(self) -> exp.Cluster:
 3257        self._match(TokenType.CLUSTER_BY)
 3258        return self.expression(
 3259            exp.Cluster(
 3260                expressions=self._parse_csv(self._parse_column),
 3261            )
 3262        )
 3263
 3264    def _parse_cluster_property(self) -> exp.ClusterProperty:
 3265        return self.expression(
 3266            exp.ClusterProperty(
 3267                expressions=self._parse_wrapped_csv(self._parse_column),
 3268            )
 3269        )
 3270
 3271    def _parse_clustered_by(self) -> exp.ClusteredByProperty:
 3272        self._match_text_seq("BY")
 3273
 3274        self._match_l_paren()
 3275        expressions = self._parse_csv(self._parse_column)
 3276        self._match_r_paren()
 3277
 3278        if self._match_text_seq("SORTED", "BY"):
 3279            self._match_l_paren()
 3280            sorted_by = self._parse_csv(self._parse_ordered)
 3281            self._match_r_paren()
 3282        else:
 3283            sorted_by = None
 3284
 3285        self._match(TokenType.INTO)
 3286        buckets = self._parse_number()
 3287        self._match_text_seq("BUCKETS")
 3288
 3289        return self.expression(
 3290            exp.ClusteredByProperty(expressions=expressions, sorted_by=sorted_by, buckets=buckets)
 3291        )
 3292
 3293    def _parse_copy_property(self) -> exp.CopyGrantsProperty | None:
 3294        if not self._match_text_seq("GRANTS"):
 3295            self._retreat(self._index - 1)
 3296            return None
 3297
 3298        return self.expression(exp.CopyGrantsProperty())
 3299
 3300    def _parse_freespace(self) -> exp.FreespaceProperty:
 3301        self._match(TokenType.EQ)
 3302        return self.expression(
 3303            exp.FreespaceProperty(this=self._parse_number(), percent=self._match(TokenType.PERCENT))
 3304        )
 3305
 3306    def _parse_mergeblockratio(
 3307        self, no: bool = False, default: bool = False
 3308    ) -> exp.MergeBlockRatioProperty:
 3309        if self._match(TokenType.EQ):
 3310            return self.expression(
 3311                exp.MergeBlockRatioProperty(
 3312                    this=self._parse_number(), percent=self._match(TokenType.PERCENT)
 3313                )
 3314            )
 3315
 3316        return self.expression(exp.MergeBlockRatioProperty(no=no, default=default))
 3317
 3318    def _parse_datablocksize(
 3319        self,
 3320        default: bool | None = None,
 3321        minimum: bool | None = None,
 3322        maximum: bool | None = None,
 3323    ) -> exp.DataBlocksizeProperty:
 3324        self._match(TokenType.EQ)
 3325        size = self._parse_number()
 3326
 3327        units = None
 3328        if self._match_texts(("BYTES", "KBYTES", "KILOBYTES")):
 3329            units = self._prev.text
 3330
 3331        return self.expression(
 3332            exp.DataBlocksizeProperty(
 3333                size=size, units=units, default=default, minimum=minimum, maximum=maximum
 3334            )
 3335        )
 3336
 3337    def _parse_blockcompression(self) -> exp.BlockCompressionProperty:
 3338        self._match(TokenType.EQ)
 3339        always = self._match_text_seq("ALWAYS")
 3340        manual = self._match_text_seq("MANUAL")
 3341        never = self._match_text_seq("NEVER")
 3342        default = self._match_text_seq("DEFAULT")
 3343
 3344        autotemp = None
 3345        if self._match_text_seq("AUTOTEMP"):
 3346            autotemp = self._parse_schema()
 3347
 3348        return self.expression(
 3349            exp.BlockCompressionProperty(
 3350                always=always, manual=manual, never=never, default=default, autotemp=autotemp
 3351            )
 3352        )
 3353
 3354    def _parse_withisolatedloading(self) -> exp.IsolatedLoadingProperty | None:
 3355        index = self._index
 3356        no = self._match_text_seq("NO")
 3357        concurrent = self._match_text_seq("CONCURRENT")
 3358
 3359        if not self._match_text_seq("ISOLATED", "LOADING"):
 3360            self._retreat(index)
 3361            return None
 3362
 3363        target = self._parse_var_from_options(self.ISOLATED_LOADING_OPTIONS, raise_unmatched=False)
 3364        return self.expression(
 3365            exp.IsolatedLoadingProperty(no=no, concurrent=concurrent, target=target)
 3366        )
 3367
 3368    def _parse_locking(self) -> exp.LockingProperty:
 3369        if self._match(TokenType.TABLE):
 3370            kind = "TABLE"
 3371        elif self._match(TokenType.VIEW):
 3372            kind = "VIEW"
 3373        elif self._match(TokenType.ROW):
 3374            kind = "ROW"
 3375        elif self._match_text_seq("DATABASE"):
 3376            kind = "DATABASE"
 3377        else:
 3378            kind = None
 3379
 3380        if kind in ("DATABASE", "TABLE", "VIEW"):
 3381            this = self._parse_table_parts()
 3382        else:
 3383            this = None
 3384
 3385        if self._match(TokenType.FOR):
 3386            for_or_in = "FOR"
 3387        elif self._match(TokenType.IN):
 3388            for_or_in = "IN"
 3389        else:
 3390            for_or_in = None
 3391
 3392        if self._match_text_seq("ACCESS"):
 3393            lock_type = "ACCESS"
 3394        elif self._match_texts(("EXCL", "EXCLUSIVE")):
 3395            lock_type = "EXCLUSIVE"
 3396        elif self._match_text_seq("SHARE"):
 3397            lock_type = "SHARE"
 3398        elif self._match_text_seq("READ"):
 3399            lock_type = "READ"
 3400        elif self._match_text_seq("WRITE"):
 3401            lock_type = "WRITE"
 3402        elif self._match_text_seq("CHECKSUM"):
 3403            lock_type = "CHECKSUM"
 3404        else:
 3405            lock_type = None
 3406
 3407        override = self._match_text_seq("OVERRIDE")
 3408
 3409        return self.expression(
 3410            exp.LockingProperty(
 3411                this=this, kind=kind, for_or_in=for_or_in, lock_type=lock_type, override=override
 3412            )
 3413        )
 3414
 3415    def _parse_partition_by(self) -> list[exp.Expr]:
 3416        if self._match(TokenType.PARTITION_BY):
 3417            return self._parse_csv(self._parse_disjunction)
 3418        return []
 3419
 3420    def _parse_partition_bound_spec(self) -> exp.PartitionBoundSpec:
 3421        def _parse_partition_bound_expr() -> exp.Expr | None:
 3422            if self._match_text_seq("MINVALUE"):
 3423                return exp.var("MINVALUE")
 3424            if self._match_text_seq("MAXVALUE"):
 3425                return exp.var("MAXVALUE")
 3426            return self._parse_bitwise()
 3427
 3428        this: exp.Expr | list[exp.Expr] | None = None
 3429        expression = None
 3430        from_expressions = None
 3431        to_expressions = None
 3432
 3433        if self._match(TokenType.IN):
 3434            this = self._parse_wrapped_csv(self._parse_bitwise)
 3435        elif self._match(TokenType.FROM):
 3436            from_expressions = self._parse_wrapped_csv(_parse_partition_bound_expr)
 3437            self._match_text_seq("TO")
 3438            to_expressions = self._parse_wrapped_csv(_parse_partition_bound_expr)
 3439        elif self._match_text_seq("WITH", "(", "MODULUS"):
 3440            this = self._parse_number()
 3441            self._match_text_seq(",", "REMAINDER")
 3442            expression = self._parse_number()
 3443            self._match_r_paren()
 3444        else:
 3445            self.raise_error("Failed to parse partition bound spec.")
 3446
 3447        return self.expression(
 3448            exp.PartitionBoundSpec(
 3449                this=this,
 3450                expression=expression,
 3451                from_expressions=from_expressions,
 3452                to_expressions=to_expressions,
 3453            )
 3454        )
 3455
 3456    # https://www.postgresql.org/docs/current/sql-createtable.html
 3457    def _parse_partitioned_of(self) -> exp.PartitionedOfProperty | None:
 3458        if not self._match_text_seq("OF"):
 3459            self._retreat(self._index - 1)
 3460            return None
 3461
 3462        this = self._parse_table(schema=True)
 3463
 3464        if self._match(TokenType.DEFAULT):
 3465            expression: exp.Var | exp.PartitionBoundSpec = exp.var("DEFAULT")
 3466        elif self._match_text_seq("FOR", "VALUES"):
 3467            expression = self._parse_partition_bound_spec()
 3468        else:
 3469            self.raise_error("Expecting either DEFAULT or FOR VALUES clause.")
 3470
 3471        return self.expression(exp.PartitionedOfProperty(this=this, expression=expression))
 3472
 3473    def _parse_partitioned_by(self) -> exp.PartitionedByProperty:
 3474        self._match(TokenType.EQ)
 3475        return self.expression(
 3476            exp.PartitionedByProperty(
 3477                this=self._parse_schema() or self._parse_bracket(self._parse_field())
 3478            )
 3479        )
 3480
 3481    def _parse_withdata(self, no: bool = False) -> exp.WithDataProperty:
 3482        if self._match_text_seq("AND", "STATISTICS"):
 3483            statistics = True
 3484        elif self._match_text_seq("AND", "NO", "STATISTICS"):
 3485            statistics = False
 3486        else:
 3487            statistics = None
 3488
 3489        return self.expression(exp.WithDataProperty(no=no, statistics=statistics))
 3490
 3491    def _parse_contains_property(self) -> exp.SqlReadWriteProperty | None:
 3492        if self._match_text_seq("SQL"):
 3493            return self.expression(exp.SqlReadWriteProperty(this="CONTAINS SQL"))
 3494        return None
 3495
 3496    def _parse_modifies_property(self) -> exp.SqlReadWriteProperty | None:
 3497        if self._match_text_seq("SQL", "DATA"):
 3498            return self.expression(exp.SqlReadWriteProperty(this="MODIFIES SQL DATA"))
 3499        return None
 3500
 3501    def _parse_no_property(self) -> exp.Expr | None:
 3502        if self._match_text_seq("PRIMARY", "INDEX"):
 3503            return exp.NoPrimaryIndexProperty()
 3504        if self._match_text_seq("SQL"):
 3505            return self.expression(exp.SqlReadWriteProperty(this="NO SQL"))
 3506        return None
 3507
 3508    def _parse_on_property(self) -> exp.Expr | None:
 3509        if self._match_text_seq("COMMIT", "PRESERVE", "ROWS"):
 3510            return exp.OnCommitProperty()
 3511        if self._match_text_seq("COMMIT", "DELETE", "ROWS"):
 3512            return exp.OnCommitProperty(delete=True)
 3513        return self.expression(exp.OnProperty(this=self._parse_schema(self._parse_id_var())))
 3514
 3515    def _parse_reads_property(self) -> exp.SqlReadWriteProperty | None:
 3516        if self._match_text_seq("SQL", "DATA"):
 3517            return self.expression(exp.SqlReadWriteProperty(this="READS SQL DATA"))
 3518        return None
 3519
 3520    def _parse_distkey(self) -> exp.DistKeyProperty:
 3521        return self.expression(exp.DistKeyProperty(this=self._parse_wrapped(self._parse_id_var)))
 3522
 3523    def _parse_create_like(self) -> exp.LikeProperty | None:
 3524        table = self._parse_table(schema=True)
 3525
 3526        options = []
 3527        while self._match_texts(("INCLUDING", "EXCLUDING")):
 3528            this = self._prev.text.upper()
 3529
 3530            id_var = self._parse_id_var()
 3531            if not id_var:
 3532                return None
 3533
 3534            options.append(
 3535                self.expression(exp.Property(this=this, value=exp.var(id_var.this.upper())))
 3536            )
 3537
 3538        return self.expression(exp.LikeProperty(this=table, expressions=options))
 3539
 3540    def _parse_sortkey(self, compound: bool = False) -> exp.SortKeyProperty:
 3541        return self.expression(
 3542            exp.SortKeyProperty(this=self._parse_wrapped_id_vars(), compound=compound)
 3543        )
 3544
 3545    def _parse_character_set(self, default: bool = False) -> exp.CharacterSetProperty:
 3546        self._match(TokenType.EQ)
 3547        return self.expression(
 3548            exp.CharacterSetProperty(this=self._parse_var_or_string(), default=default)
 3549        )
 3550
 3551    def _parse_remote_with_connection(self) -> exp.RemoteWithConnectionModelProperty:
 3552        self._match_text_seq("WITH", "CONNECTION")
 3553        return self.expression(
 3554            exp.RemoteWithConnectionModelProperty(this=self._parse_table_parts())
 3555        )
 3556
 3557    def _parse_returns(self) -> exp.ReturnsProperty:
 3558        value: exp.Expr | None
 3559        null = None
 3560        is_table = self._match(TokenType.TABLE)
 3561
 3562        if is_table:
 3563            if self._match(TokenType.LT):
 3564                value = self.expression(
 3565                    exp.Schema(this="TABLE", expressions=self._parse_csv(self._parse_struct_types))
 3566                )
 3567                if not self._match(TokenType.GT):
 3568                    self.raise_error("Expecting >")
 3569            else:
 3570                value = self._parse_schema(exp.var("TABLE"))
 3571        elif self._match_text_seq("NULL", "ON", "NULL", "INPUT"):
 3572            null = True
 3573            value = None
 3574        else:
 3575            value = self._parse_types()
 3576
 3577        return self.expression(exp.ReturnsProperty(this=value, is_table=is_table, null=null))
 3578
 3579    def _parse_describe(self) -> exp.Describe:
 3580        kind = self._prev.text if self._match_set(self.CREATABLES) else None
 3581        style: str | None = (
 3582            self._prev.text.upper() if self._match_texts(self.DESCRIBE_STYLES) else None
 3583        )
 3584        if self._match(TokenType.DOT):
 3585            style = None
 3586            self._retreat(self._index - 2)
 3587
 3588        format = self._parse_property() if self._match(TokenType.FORMAT, advance=False) else None
 3589
 3590        if self._match_set(self.STATEMENT_PARSERS, advance=False):
 3591            this = self._parse_statement()
 3592        else:
 3593            this = self._parse_table(schema=True)
 3594
 3595        properties = self._parse_properties()
 3596        expressions = properties.expressions if properties else None
 3597        partition = self._parse_partition()
 3598        return self.expression(
 3599            exp.Describe(
 3600                this=this,
 3601                style=style,
 3602                kind=kind,
 3603                expressions=expressions,
 3604                partition=partition,
 3605                format=format,
 3606                as_json=self._match_text_seq("AS", "JSON"),
 3607            )
 3608        )
 3609
 3610    def _parse_multitable_inserts(
 3611        self, comments: list[str] | None, kind: str | None = None
 3612    ) -> exp.MultitableInserts:
 3613        kind = kind or self._prev.text.upper()
 3614        expressions = []
 3615
 3616        def parse_conditional_insert() -> exp.ConditionalInsert | None:
 3617            if self._match(TokenType.WHEN):
 3618                expression = self._parse_disjunction()
 3619                self._match(TokenType.THEN)
 3620            else:
 3621                expression = None
 3622
 3623            else_ = self._match(TokenType.ELSE)
 3624
 3625            if not self._match(TokenType.INTO):
 3626                return None
 3627
 3628            return self.expression(
 3629                exp.ConditionalInsert(
 3630                    this=self.expression(
 3631                        exp.Insert(
 3632                            this=self._parse_table(schema=True),
 3633                            expression=self._parse_derived_table_values(),
 3634                        )
 3635                    ),
 3636                    expression=expression,
 3637                    else_=else_,
 3638                )
 3639            )
 3640
 3641        expression = parse_conditional_insert()
 3642        while expression is not None:
 3643            expressions.append(expression)
 3644            expression = parse_conditional_insert()
 3645
 3646        return self.expression(
 3647            exp.MultitableInserts(kind=kind, expressions=expressions, source=self._parse_table()),
 3648            comments=comments,
 3649        )
 3650
 3651    def _parse_insert(self) -> exp.Insert | exp.MultitableInserts:
 3652        comments: list[str] = []
 3653        hint = self._parse_hint()
 3654        overwrite = self._match(TokenType.OVERWRITE)
 3655        ignore = self._match(TokenType.IGNORE)
 3656        local = self._match_text_seq("LOCAL")
 3657        alternative = None
 3658        is_function = None
 3659
 3660        if self._match_text_seq("DIRECTORY"):
 3661            this: exp.Expr | None = self.expression(
 3662                exp.Directory(
 3663                    this=self._parse_var_or_string(),
 3664                    local=local,
 3665                    row_format=self._parse_row_format(match_row=True),
 3666                )
 3667            )
 3668        else:
 3669            if self._match_set((TokenType.FIRST, TokenType.ALL)):
 3670                comments += ensure_list(self._prev_comments)
 3671                return self._parse_multitable_inserts(comments)
 3672
 3673            # Oracle's conditional form defaults to ALL when the keyword is omitted
 3674            if self._match(TokenType.WHEN, advance=False):
 3675                return self._parse_multitable_inserts(comments, kind="ALL")
 3676
 3677            if self._match(TokenType.OR):
 3678                alternative = self._match_texts(self.INSERT_ALTERNATIVES) and self._prev.text
 3679
 3680            self._match(TokenType.INTO)
 3681            comments += ensure_list(self._prev_comments)
 3682            self._match(TokenType.TABLE)
 3683            is_function = self._match(TokenType.FUNCTION)
 3684
 3685            this = self._parse_function() if is_function else self._parse_insert_table()
 3686
 3687        # MySQL's INSERT ... SET is normalized into the INSERT ... (cols) VALUES (vals) variant
 3688        set_values = None
 3689        if self._match(TokenType.SET):
 3690            columns = []
 3691            values = []
 3692
 3693            def _parse_set_assignment() -> exp.Expr | None:
 3694                target = self._parse_column()
 3695                if isinstance(target, exp.Column) and self._match(TokenType.EQ):
 3696                    if self.dialect.SUPPORTS_VALUES_DEFAULT and self._match(TokenType.DEFAULT):
 3697                        value: exp.Expr | None = exp.var(self._prev.text.upper())
 3698                    else:
 3699                        value = self._parse_disjunction()
 3700
 3701                    if value:
 3702                        columns.append(target.this)
 3703                        values.append(value)
 3704                        return value
 3705
 3706                self.raise_error("Expected column assignment in INSERT ... SET")
 3707                return None
 3708
 3709            self._parse_csv(_parse_set_assignment)
 3710
 3711            this = self.expression(exp.Schema(this=this, expressions=columns))
 3712            set_values = self.expression(
 3713                exp.Values(
 3714                    expressions=[exp.Tuple(expressions=values)],
 3715                    alias=self._parse_table_alias(),
 3716                )
 3717            )
 3718
 3719        returning = self._parse_returning()  # TSQL allows RETURNING before source
 3720
 3721        stored = self._match_text_seq("STORED") and self._parse_stored()
 3722        by_name = self._match_text_seq("BY", "NAME")
 3723        exists = self._parse_exists()
 3724        replace_where = None
 3725        replace_using = None
 3726
 3727        if self._match(TokenType.REPLACE):
 3728            if self._match(TokenType.WHERE):
 3729                replace_where = self._parse_disjunction()
 3730            elif self._match(TokenType.USING):
 3731                replace_using = self._parse_using_identifiers()
 3732
 3733        return self.expression(
 3734            exp.Insert(
 3735                hint=hint,
 3736                is_function=is_function,
 3737                this=this,
 3738                stored=stored,
 3739                by_name=by_name,
 3740                exists=exists,
 3741                where=replace_where,
 3742                using=replace_using,
 3743                partition=self._match(TokenType.PARTITION_BY) and self._parse_partitioned_by(),
 3744                settings=self._match_text_seq("SETTINGS") and self._parse_settings_property(),
 3745                default=self._match_text_seq("DEFAULT", "VALUES"),
 3746                expression=set_values
 3747                or self._parse_derived_table_values(allow_value_synonym=True)
 3748                or self._parse_ddl_select(),
 3749                conflict=self._parse_on_conflict(),
 3750                returning=returning or self._parse_returning(),
 3751                overwrite=overwrite,
 3752                alternative=alternative,
 3753                ignore=ignore,
 3754                source=self._match(TokenType.TABLE) and self._parse_table(),
 3755            ),
 3756            comments=comments,
 3757        )
 3758
 3759    def _parse_insert_table(self) -> exp.Expr | None:
 3760        this = self._parse_table(schema=True, parse_partition=True)
 3761        if isinstance(this, exp.Table) and self._match(TokenType.ALIAS, advance=False):
 3762            this.set("alias", self._parse_table_alias())
 3763        return this
 3764
 3765    def _parse_kill(self) -> exp.Kill:
 3766        kind = exp.var(self._prev.text) if self._match_texts(("CONNECTION", "QUERY")) else None
 3767
 3768        return self.expression(exp.Kill(this=self._parse_primary(), kind=kind))
 3769
 3770    def _parse_on_conflict(self) -> exp.OnConflict | None:
 3771        conflict = self._match_text_seq("ON", "CONFLICT")
 3772        duplicate = self._match_text_seq("ON", "DUPLICATE", "KEY")
 3773
 3774        if not conflict and not duplicate:
 3775            return None
 3776
 3777        conflict_keys = None
 3778        constraint = None
 3779
 3780        if conflict:
 3781            if self._match_text_seq("ON", "CONSTRAINT"):
 3782                constraint = self._parse_id_var()
 3783            elif self._match(TokenType.L_PAREN):
 3784                conflict_keys = self._parse_csv(self._parse_indexed_column)
 3785                self._match_r_paren()
 3786
 3787        index_predicate = self._parse_where()
 3788
 3789        action = self._parse_var_from_options(self.CONFLICT_ACTIONS)
 3790        if self._prev.token_type == TokenType.UPDATE:
 3791            self._match(TokenType.SET)
 3792            expressions = self._parse_csv(self._parse_update_assignment)
 3793        else:
 3794            expressions = None
 3795
 3796        return self.expression(
 3797            exp.OnConflict(
 3798                duplicate=duplicate,
 3799                expressions=expressions,
 3800                action=action,
 3801                conflict_keys=conflict_keys,
 3802                index_predicate=index_predicate,
 3803                constraint=constraint,
 3804                where=self._parse_where(),
 3805            )
 3806        )
 3807
 3808    def _parse_returning(self) -> exp.Returning | None:
 3809        if not self._match(TokenType.RETURNING):
 3810            return None
 3811        return self.expression(
 3812            exp.Returning(
 3813                expressions=self._parse_csv(self._parse_expression),
 3814                into=self._match(TokenType.INTO) and self._parse_table_part(),
 3815            )
 3816        )
 3817
 3818    def _parse_row(self) -> exp.RowFormatSerdeProperty | exp.RowFormatDelimitedProperty | None:
 3819        if not self._match(TokenType.FORMAT):
 3820            return None
 3821        return self._parse_row_format()
 3822
 3823    def _parse_serde_properties(self, with_: bool = False) -> exp.SerdeProperties | None:
 3824        index = self._index
 3825        with_ = with_ or self._match_text_seq("WITH")
 3826
 3827        if not self._match(TokenType.SERDE_PROPERTIES):
 3828            self._retreat(index)
 3829            return None
 3830        return self.expression(
 3831            exp.SerdeProperties(expressions=self._parse_wrapped_properties(), with_=with_)
 3832        )
 3833
 3834    def _parse_row_format(
 3835        self, match_row: bool = False
 3836    ) -> exp.RowFormatSerdeProperty | exp.RowFormatDelimitedProperty | None:
 3837        if match_row and not self._match_pair(TokenType.ROW, TokenType.FORMAT):
 3838            return None
 3839
 3840        if self._match_text_seq("SERDE"):
 3841            this = self._parse_string()
 3842
 3843            serde_properties = self._parse_serde_properties()
 3844
 3845            return self.expression(
 3846                exp.RowFormatSerdeProperty(this=this, serde_properties=serde_properties)
 3847            )
 3848
 3849        self._match_text_seq("DELIMITED")
 3850
 3851        kwargs = {}
 3852
 3853        if self._match_text_seq("FIELDS", "TERMINATED", "BY"):
 3854            kwargs["fields"] = self._parse_string()
 3855            if self._match_text_seq("ESCAPED", "BY"):
 3856                kwargs["escaped"] = self._parse_string()
 3857        if self._match_text_seq("COLLECTION", "ITEMS", "TERMINATED", "BY"):
 3858            kwargs["collection_items"] = self._parse_string()
 3859        if self._match_text_seq("MAP", "KEYS", "TERMINATED", "BY"):
 3860            kwargs["map_keys"] = self._parse_string()
 3861        if self._match_text_seq("LINES", "TERMINATED", "BY"):
 3862            kwargs["lines"] = self._parse_string()
 3863        if self._match_text_seq("NULL", "DEFINED", "AS"):
 3864            kwargs["null"] = self._parse_string()
 3865
 3866        return self.expression(exp.RowFormatDelimitedProperty(**kwargs))  # type: ignore
 3867
 3868    def _parse_load(self) -> exp.LoadData | exp.Command:
 3869        if self._match_text_seq("DATA"):
 3870            local = self._match_text_seq("LOCAL")
 3871            self._match_text_seq("INPATH")
 3872            inpath = self._parse_string()
 3873            overwrite = self._match(TokenType.OVERWRITE)
 3874            temp: bool | None = None
 3875            if self._match(TokenType.INTO):
 3876                temp = self._match(TokenType.TEMPORARY)
 3877                self._match(TokenType.TABLE)
 3878
 3879            return self.expression(
 3880                exp.LoadData(
 3881                    this=self._parse_table(schema=True),
 3882                    local=local,
 3883                    overwrite=overwrite,
 3884                    temp=temp,
 3885                    inpath=inpath,
 3886                    files=self._match_text_seq("FROM", "FILES")
 3887                    and exp.Properties(expressions=self._parse_wrapped_properties()),
 3888                    partition=self._parse_partition(),
 3889                    input_format=self._match_text_seq("INPUTFORMAT") and self._parse_string(),
 3890                    serde=self._match_text_seq("SERDE") and self._parse_string(),
 3891                )
 3892            )
 3893        return self._parse_as_command(self._prev)
 3894
 3895    def _parse_delete(self) -> exp.Delete:
 3896        hint = self._parse_hint()
 3897
 3898        # This handles MySQL's "Multiple-Table Syntax"
 3899        # https://dev.mysql.com/doc/refman/8.0/en/delete.html
 3900        tables = None
 3901        if not self._match(TokenType.FROM, advance=False):
 3902            tables = self._parse_csv(self._parse_table) or None
 3903
 3904        returning = self._parse_returning()
 3905
 3906        return self.expression(
 3907            exp.Delete(
 3908                hint=hint,
 3909                tables=tables,
 3910                this=self._match(TokenType.FROM) and self._parse_table(joins=True),
 3911                using=self._match(TokenType.USING)
 3912                and self._parse_csv(lambda: self._parse_table(joins=True)),
 3913                cluster=self._match(TokenType.ON) and self._parse_on_property(),
 3914                where=self._parse_where(),
 3915                returning=returning or self._parse_returning(),
 3916                order=self._parse_order(),
 3917                limit=self._parse_limit(),
 3918            )
 3919        )
 3920
 3921    def _parse_update_assignment(self) -> exp.Expr | None:
 3922        this = self._parse_comparison()
 3923        if self._match(TokenType.EQ):
 3924            comments = self._prev_comments
 3925            this = self.expression(
 3926                exp.EQ(this=this, expression=self._parse_disjunction()), comments=comments
 3927            )
 3928        return this
 3929
 3930    def _parse_update(self) -> exp.Update:
 3931        hint = self._parse_hint()
 3932        kwargs: dict[str, object] = {
 3933            "hint": hint,
 3934            "this": self._parse_table(joins=True, alias_tokens=self.UPDATE_ALIAS_TOKENS),
 3935        }
 3936        while self._curr:
 3937            if self._match(TokenType.SET):
 3938                kwargs["expressions"] = self._parse_csv(self._parse_update_assignment)
 3939            elif self._match(TokenType.RETURNING, advance=False):
 3940                kwargs["returning"] = self._parse_returning()
 3941            elif self._match(TokenType.FROM, advance=False):
 3942                from_ = self._parse_from(joins=True)
 3943                table = from_.this if from_ else None
 3944                if isinstance(table, exp.Subquery) and self._match(TokenType.JOIN, advance=False):
 3945                    table.set("joins", list(self._parse_joins()) or None)
 3946
 3947                kwargs["from_"] = from_
 3948            elif self._match(TokenType.WHERE, advance=False):
 3949                kwargs["where"] = self._parse_where()
 3950            elif self._match(TokenType.ORDER_BY, advance=False):
 3951                kwargs["order"] = self._parse_order()
 3952            elif self._match(TokenType.LIMIT, advance=False):
 3953                kwargs["limit"] = self._parse_limit()
 3954            else:
 3955                break
 3956
 3957        return self.expression(exp.Update(**kwargs))
 3958
 3959    def _parse_use(self) -> exp.Use:
 3960        return self.expression(
 3961            exp.Use(
 3962                kind=self._parse_var_from_options(self.USABLES, raise_unmatched=False),
 3963                this=self._parse_table(schema=False),
 3964            )
 3965        )
 3966
 3967    def _parse_uncache(self) -> exp.Uncache:
 3968        if not self._match(TokenType.TABLE):
 3969            self.raise_error("Expecting TABLE after UNCACHE")
 3970
 3971        return self.expression(
 3972            exp.Uncache(exists=self._parse_exists(), this=self._parse_table(schema=True))
 3973        )
 3974
 3975    def _parse_cache(self) -> exp.Cache:
 3976        lazy = self._match_text_seq("LAZY")
 3977        self._match(TokenType.TABLE)
 3978        table = self._parse_table(schema=True)
 3979
 3980        options = []
 3981        if self._match_text_seq("OPTIONS"):
 3982            self._match_l_paren()
 3983            k = self._parse_string()
 3984            self._match(TokenType.EQ)
 3985            v = self._parse_string()
 3986            options = [k, v]
 3987            self._match_r_paren()
 3988
 3989        self._match(TokenType.ALIAS)
 3990        return self.expression(
 3991            exp.Cache(
 3992                this=table, lazy=lazy, options=options, expression=self._parse_select(nested=True)
 3993            )
 3994        )
 3995
 3996    def _parse_partition(self) -> exp.Partition | None:
 3997        if not self._match_texts(self.PARTITION_KEYWORDS):
 3998            return None
 3999
 4000        return self.expression(
 4001            exp.Partition(
 4002                subpartition=self._prev.text.upper() == "SUBPARTITION",
 4003                expressions=self._parse_wrapped_csv(self._parse_disjunction),
 4004            )
 4005        )
 4006
 4007    def _parse_value(self, values: bool = True) -> exp.Tuple | None:
 4008        def _parse_value_expression() -> exp.Expr | None:
 4009            if self.dialect.SUPPORTS_VALUES_DEFAULT and self._match(TokenType.DEFAULT):
 4010                return exp.var(self._prev.text.upper())
 4011            return self._parse_expression()
 4012
 4013        if self._match(TokenType.L_PAREN):
 4014            expressions = self._parse_csv(_parse_value_expression)
 4015            self._match_r_paren()
 4016            return self.expression(exp.Tuple(expressions=expressions))
 4017
 4018        # In some dialects we can have VALUES 1, 2 which results in 1 column & 2 rows.
 4019        expression = self._parse_expression()
 4020        if expression:
 4021            return self.expression(exp.Tuple(expressions=[expression]))
 4022        return None
 4023
 4024    def _parse_projections(
 4025        self,
 4026    ) -> tuple[list[exp.Expr], list[exp.Expr] | None]:
 4027        return self._parse_expressions(), None
 4028
 4029    def _parse_wrapped_select(self, table: bool = False) -> exp.Expr | None:
 4030        if self._match_set((TokenType.PIVOT, TokenType.UNPIVOT)):
 4031            this: exp.Expr | None = self._parse_simplified_pivot(
 4032                is_unpivot=self._prev.token_type == TokenType.UNPIVOT
 4033            )
 4034        else:
 4035            # Support parentheses for duckdb FROM-first syntax
 4036            this = (
 4037                self._parse_table(consume_pipe=True)
 4038                if table and not self._match(TokenType.FROM, advance=False)
 4039                else self._parse_select(nested=True, parse_set_operation=False)
 4040            )
 4041
 4042            # Transform exp.Values into a exp.Table to pass through parse_query_modifiers
 4043            # in case a modifier (e.g. join) is following
 4044            if table and isinstance(this, exp.Values) and this.alias:
 4045                alias = this.args["alias"].pop()
 4046                this = exp.Table(this=this, alias=alias)
 4047
 4048            this = self._parse_query_modifiers(self._parse_set_operations(this))
 4049
 4050        return this
 4051
 4052    def _parse_select(
 4053        self,
 4054        nested: bool = False,
 4055        table: bool = False,
 4056        parse_subquery_alias: bool = True,
 4057        parse_set_operation: bool = True,
 4058        consume_pipe: bool = True,
 4059    ) -> exp.Expr | None:
 4060        query = self._parse_select_query(
 4061            nested=nested,
 4062            table=table,
 4063            parse_subquery_alias=parse_subquery_alias,
 4064            parse_set_operation=parse_set_operation,
 4065        )
 4066
 4067        if consume_pipe and self._match(TokenType.PIPE_GT, advance=False):
 4068            if isinstance(query, exp.Query):
 4069                query = self._parse_pipe_syntax_query(query)
 4070                query = query.subquery(copy=False) if query and table else query
 4071
 4072        return query
 4073
 4074    def _parse_select_query(
 4075        self,
 4076        nested: bool = False,
 4077        table: bool = False,
 4078        parse_subquery_alias: bool = True,
 4079        parse_set_operation: bool = True,
 4080    ) -> exp.Expr | None:
 4081        cte = self._parse_with()
 4082
 4083        if cte:
 4084            this = self._parse_statement()
 4085
 4086            if not this:
 4087                self.raise_error("Failed to parse any statement following CTE")
 4088                return cte
 4089
 4090            while isinstance(this, exp.Subquery) and this.is_wrapper:
 4091                this = this.this
 4092
 4093            assert this is not None
 4094            if "with_" in this.arg_types:
 4095                if inner_cte := this.args.get("with_"):
 4096                    cte.set("expressions", cte.expressions + inner_cte.expressions)
 4097                    if inner_cte.args.get("recursive"):
 4098                        cte.set("recursive", True)
 4099                this.set("with_", cte)
 4100            else:
 4101                self.raise_error(f"{this.key} does not support CTE")
 4102                this = cte
 4103
 4104            return this
 4105
 4106        # duckdb supports leading with FROM x
 4107        from_ = (
 4108            self._parse_from(joins=True, consume_pipe=True)
 4109            if self._match(TokenType.FROM, advance=False)
 4110            else None
 4111        )
 4112        laterals = list(iter(self._parse_lateral, None)) if from_ else None
 4113
 4114        # Hive supports multi-table inserts: FROM x INSERT ... SELECT ... [INSERT ... SELECT ...]
 4115        if from_ and self._match(TokenType.INSERT, advance=False):
 4116            source = from_.this
 4117            source.set("laterals", laterals or None)
 4118
 4119            inserts = []
 4120            while self._match(TokenType.INSERT, advance=False):
 4121                inserts.append(self._parse_statement())
 4122
 4123            return self.expression(
 4124                exp.MultitableInserts(expressions=inserts, source=source),
 4125                comments=from_.comments,
 4126            )
 4127
 4128        if self._match(TokenType.SELECT):
 4129            comments = self._prev_comments
 4130
 4131            hint = self._parse_hint()
 4132
 4133            if self._next and not self._next.token_type == TokenType.DOT:
 4134                all_ = self._match(TokenType.ALL)
 4135                matched_distinct = self._match_set(self.DISTINCT_TOKENS)
 4136            else:
 4137                all_, matched_distinct = None, False
 4138
 4139            kind = (
 4140                self._prev.text.upper()
 4141                if self._match(TokenType.ALIAS) and self._match_texts(("STRUCT", "VALUE"))
 4142                else None
 4143            )
 4144
 4145            distinct: exp.Expr | None = (
 4146                self.expression(
 4147                    exp.Distinct(
 4148                        on=self._parse_value(values=False) if self._match(TokenType.ON) else None
 4149                    )
 4150                )
 4151                if matched_distinct
 4152                else None
 4153            )
 4154
 4155            operation_modifiers = []
 4156            while self._curr and self._match_texts(self.OPERATION_MODIFIERS):
 4157                operation_modifiers.append(exp.var(self._prev.text.upper()))
 4158
 4159            limit = self._parse_limit(top=True)
 4160
 4161            # Some dialects (e.g. Redshift, T-SQL) allow SELECT TOP N DISTINCT ...
 4162            if limit and not matched_distinct and not all_:
 4163                matched_distinct = self._match_set(self.DISTINCT_TOKENS)
 4164                if matched_distinct:
 4165                    distinct = self.expression(
 4166                        exp.Distinct(
 4167                            on=self._parse_value(values=False)
 4168                            if self._match(TokenType.ON)
 4169                            else None
 4170                        )
 4171                    )
 4172                else:
 4173                    all_ = self._match(TokenType.ALL)
 4174
 4175            if all_ and distinct:
 4176                self.raise_error("Cannot specify both ALL and DISTINCT after SELECT")
 4177
 4178            projections, exclude = self._parse_projections()
 4179
 4180            this = self.expression(
 4181                exp.Select(
 4182                    kind=kind,
 4183                    hint=hint,
 4184                    distinct=distinct,
 4185                    expressions=projections,
 4186                    limit=limit,
 4187                    exclude=exclude,
 4188                    operation_modifiers=operation_modifiers or None,
 4189                )
 4190            )
 4191            this.comments = comments
 4192
 4193            into = self._parse_into()
 4194            if into:
 4195                this.set("into", into)
 4196
 4197            if not from_:
 4198                from_ = self._parse_from()
 4199
 4200            if from_:
 4201                this.set("from_", from_)
 4202
 4203            this = self._parse_query_modifiers(this)
 4204        elif (table or nested) and self._match(TokenType.L_PAREN):
 4205            comments = self._prev_comments
 4206            this = self._parse_wrapped_select(table=table)
 4207
 4208            if this:
 4209                this.add_comments(comments, prepend=True)
 4210
 4211            # We return early here so that the UNION isn't attached to the subquery by the
 4212            # following call to _parse_set_operations, but instead becomes the parent node
 4213            self._match_r_paren()
 4214            return self._parse_subquery(this, parse_alias=parse_subquery_alias)
 4215        elif self._match(TokenType.VALUES, advance=False):
 4216            this = self._parse_derived_table_values()
 4217        elif from_:
 4218            this = exp.select("*").from_(from_.this, copy=False)
 4219            this = self._parse_query_modifiers(this)
 4220        elif self._match(TokenType.SUMMARIZE):
 4221            table = self._match(TokenType.TABLE)
 4222            this = self._parse_select() or self._parse_string() or self._parse_table()
 4223            return self.expression(exp.Summarize(this=this, table=table))
 4224        elif self._match(TokenType.DESCRIBE):
 4225            this = self._parse_describe()
 4226        else:
 4227            this = None
 4228
 4229        if laterals and isinstance(this, exp.Select):
 4230            this.set("laterals", [*laterals, *(this.args.get("laterals") or [])])
 4231
 4232        return self._parse_set_operations(this) if parse_set_operation else this
 4233
 4234    def _parse_recursive_with_search(self) -> exp.RecursiveWithSearch | None:
 4235        self._match_text_seq("SEARCH")
 4236
 4237        kind = self._match_texts(self.RECURSIVE_CTE_SEARCH_KIND) and self._prev.text.upper()
 4238
 4239        if not kind:
 4240            return None
 4241
 4242        self._match_text_seq("FIRST", "BY")
 4243
 4244        return self.expression(
 4245            exp.RecursiveWithSearch(
 4246                kind=kind,
 4247                this=self._parse_id_var(),
 4248                expression=self._match_text_seq("SET") and self._parse_id_var(),
 4249                to=self._match_text_seq("TO") and self._parse_bitwise(),
 4250                default=self._match(TokenType.DEFAULT) and self._parse_bitwise(),
 4251                using=self._match_text_seq("USING") and self._parse_id_var(),
 4252            )
 4253        )
 4254
 4255    def _parse_with(self, skip_with_token: bool = False) -> exp.With | None:
 4256        if not skip_with_token and not self._match(TokenType.WITH):
 4257            return None
 4258
 4259        comments = self._prev_comments
 4260        recursive = self._match(TokenType.RECURSIVE)
 4261
 4262        last_comments = None
 4263        expressions = []
 4264        udfs = []
 4265        while True:
 4266            cte = self._parse_cte()
 4267            if cte:
 4268                if isinstance(cte, exp.FunctionSpecification):
 4269                    udfs.append(cte)
 4270                else:
 4271                    expressions.append(cte)
 4272
 4273                if last_comments:
 4274                    cte.add_comments(last_comments)
 4275
 4276            if not self._match(TokenType.COMMA) and not self._match(TokenType.WITH):
 4277                break
 4278            else:
 4279                self._match(TokenType.WITH)
 4280                recursive = self._match(TokenType.RECURSIVE) or recursive
 4281
 4282            last_comments = self._prev_comments
 4283
 4284        search = self._parse_recursive_with_search()
 4285
 4286        # A SEARCH clause can be followed by a CYCLE clause
 4287        cycle = (
 4288            self._parse_recursive_with_search()
 4289            if search
 4290            and search.args.get("kind") != "CYCLE"
 4291            and self._match_text_seq("CYCLE", advance=False)
 4292            else None
 4293        )
 4294
 4295        return self.expression(
 4296            exp.With(
 4297                expressions=expressions,
 4298                recursive=recursive or None,
 4299                search=search,
 4300                cycle=cycle,
 4301                udfs=udfs or None,
 4302            ),
 4303            comments=comments,
 4304        )
 4305
 4306    def _parse_cte(self) -> exp.CTE | exp.FunctionSpecification | None:
 4307        index = self._index
 4308
 4309        alias = self._parse_table_alias(self.ID_VAR_TOKENS)
 4310        if not alias or not alias.this:
 4311            self.raise_error("Expected CTE to have alias")
 4312
 4313        key_expressions = (
 4314            self._parse_wrapped_id_vars() if self._match_text_seq("USING", "KEY") else None
 4315        )
 4316
 4317        if not self._match(TokenType.ALIAS) and not self.OPTIONAL_ALIAS_TOKEN_CTE:
 4318            self._retreat(index)
 4319            return None
 4320
 4321        comments = self._prev_comments
 4322
 4323        if self._match_text_seq("NOT", "MATERIALIZED"):
 4324            materialized = False
 4325        elif self._match_text_seq("MATERIALIZED"):
 4326            materialized = True
 4327        else:
 4328            materialized = None
 4329
 4330        cte = self.expression(
 4331            exp.CTE(
 4332                this=self._parse_wrapped(self._parse_statement),
 4333                alias=alias,
 4334                materialized=materialized,
 4335                key_expressions=key_expressions,
 4336            ),
 4337            comments=comments,
 4338        )
 4339
 4340        values = cte.this
 4341        if isinstance(values, exp.Values):
 4342            cte.set("this", self._values_to_select(values))
 4343
 4344        return cte
 4345
 4346    def _values_to_select(self, values: exp.Values) -> exp.Select:
 4347        if values.alias:
 4348            return exp.select("*").from_(values)
 4349        return exp.select("*").from_(exp.alias_(values, "_values", table=True))
 4350
 4351    def _parse_table_alias(
 4352        self, alias_tokens: t.Collection[TokenType] | None = None
 4353    ) -> exp.TableAlias | None:
 4354        # In some dialects, LIMIT and OFFSET can act as both identifiers and keywords (clauses)
 4355        # so this section tries to parse the clause version and if it fails, it treats the token
 4356        # as an identifier (alias)
 4357        if self._can_parse_limit_or_offset():
 4358            return None
 4359
 4360        # START is never treated as an implicit alias when followed by WITH, since that
 4361        # would swallow the beginning of a START WITH ... CONNECT BY clause
 4362        if self._curr.text.upper() == "START" and self._next.text.upper() == "WITH":
 4363            return None
 4364
 4365        any_token = self._match(TokenType.ALIAS)
 4366        alias = (
 4367            self._parse_id_var(any_token=any_token, tokens=alias_tokens or self.TABLE_ALIAS_TOKENS)
 4368            or self._parse_string_as_identifier()
 4369        )
 4370
 4371        index = self._index
 4372        if self._match(TokenType.L_PAREN):
 4373            columns = self._parse_csv(self._parse_function_parameter)
 4374            self._match_r_paren() if columns else self._retreat(index)
 4375        else:
 4376            columns = None
 4377
 4378        if not alias and not columns:
 4379            return None
 4380
 4381        table_alias = self.expression(exp.TableAlias(this=alias, columns=columns))
 4382
 4383        # We bubble up comments from the Identifier to the TableAlias
 4384        if isinstance(alias, exp.Identifier):
 4385            table_alias.add_comments(alias.pop_comments())
 4386
 4387        return table_alias
 4388
 4389    def _parse_subquery(
 4390        self, this: exp.Expr | None, parse_alias: bool = True
 4391    ) -> exp.Subquery | None:
 4392        if not this:
 4393            return None
 4394
 4395        return self.expression(
 4396            exp.Subquery(
 4397                this=this,
 4398                pivots=self._parse_pivots(),
 4399                alias=self._parse_table_alias() if parse_alias else None,
 4400                sample=self._parse_table_sample(),
 4401            )
 4402        )
 4403
 4404    def _implicit_unnests_to_explicit(self, this: E) -> E:
 4405        from sqlglot.optimizer.normalize_identifiers import normalize_identifiers as _norm
 4406
 4407        refs = {_norm(this.args["from_"].this.copy(), dialect=self.dialect).alias_or_name}
 4408        for i, join in enumerate(this.args.get("joins") or []):
 4409            table = join.this
 4410            normalized_table = table.copy()
 4411            normalized_table.meta["maybe_column"] = True
 4412            normalized_table = _norm(normalized_table, dialect=self.dialect)
 4413
 4414            if isinstance(table, exp.Table) and not join.args.get("on"):
 4415                if len(normalized_table.parts) > 1 and normalized_table.parts[0].name in refs:
 4416                    table_as_column = table.to_column()
 4417                    unnest = exp.Unnest(expressions=[table_as_column])
 4418
 4419                    # Table.to_column creates a parent Alias node that we want to convert to
 4420                    # a TableAlias and attach to the Unnest, so it matches the parser's output
 4421                    if isinstance(table.args.get("alias"), exp.TableAlias):
 4422                        table_as_column.replace(table_as_column.this)
 4423                        exp.alias_(unnest, None, table=[table.args["alias"].this], copy=False)
 4424
 4425                    table.replace(unnest)
 4426
 4427            refs.add(normalized_table.alias_or_name)
 4428
 4429        return this
 4430
 4431    @t.overload
 4432    def _parse_query_modifiers(self, this: E) -> E: ...
 4433
 4434    @t.overload
 4435    def _parse_query_modifiers(self, this: None) -> None: ...
 4436
 4437    def _parse_query_modifiers(self, this):
 4438        if isinstance(this, self.MODIFIABLES):
 4439            for join in self._parse_joins():
 4440                this.append("joins", join)
 4441            for lateral in iter(self._parse_lateral, None):
 4442                this.append("laterals", lateral)
 4443
 4444            while True:
 4445                if self._match_set(self.QUERY_MODIFIER_PARSERS, advance=False):
 4446                    modifier_token = self._curr
 4447
 4448                    # Defer LIMIT/FETCH after TOP until a set op is built so it applies to the whole result
 4449                    # e.g., SELECT 1 AS x UNION ALL SELECT TOP 2 2 AS x LIMIT 1 -> limit applies to union
 4450                    if (
 4451                        modifier_token.token_type in (TokenType.LIMIT, TokenType.FETCH)
 4452                        and (limit := this.args.get("limit"))
 4453                        and limit.meta.get("top")
 4454                    ):
 4455                        break
 4456
 4457                    parser = self.QUERY_MODIFIER_PARSERS[modifier_token.token_type]
 4458                    key, expression = parser(self)
 4459
 4460                    if expression:
 4461                        if this.args.get(key):
 4462                            self.raise_error(
 4463                                f"Found multiple '{modifier_token.text.upper()}' clauses",
 4464                                token=modifier_token,
 4465                            )
 4466
 4467                        this.set(key, expression)
 4468                        if key == "limit":
 4469                            offset = expression.args.get("offset")
 4470                            expression.set("offset", None)
 4471
 4472                            if offset:
 4473                                if this.args.get("offset"):
 4474                                    self.raise_error(
 4475                                        "Found multiple 'OFFSET' clauses", token=modifier_token
 4476                                    )
 4477
 4478                                offset = exp.Offset(expression=offset)
 4479                                this.set("offset", offset)
 4480
 4481                                limit_by_expressions = expression.expressions
 4482                                expression.set("expressions", None)
 4483                                offset.set("expressions", limit_by_expressions)
 4484                        continue
 4485
 4486                if self._curr.text.upper() == "START":
 4487                    modifier_token = self._curr
 4488                    connect = self._parse_connect()
 4489                    if connect:
 4490                        if this.args.get("connect"):
 4491                            self.raise_error(
 4492                                "Found multiple 'START WITH' clauses", token=modifier_token
 4493                            )
 4494
 4495                        this.set("connect", connect)
 4496                        continue
 4497                break
 4498
 4499        if self.SUPPORTS_IMPLICIT_UNNEST and this and this.args.get("from_"):
 4500            this = self._implicit_unnests_to_explicit(this)
 4501
 4502        return this
 4503
 4504    def _parse_hint_fallback_to_string(self) -> exp.Hint | None:
 4505        start = self._curr
 4506        while self._curr:
 4507            self._advance()
 4508
 4509        end = self._tokens[self._index - 1]
 4510        return exp.Hint(expressions=[self._find_sql(start, end)])
 4511
 4512    def _parse_hint_function_call(self) -> exp.Expr | None:
 4513        return self._parse_function_call()
 4514
 4515    def _parse_hint_body(self) -> exp.Hint | None:
 4516        start_index = self._index
 4517        should_fallback_to_string = False
 4518
 4519        hints = []
 4520        try:
 4521            for hint in iter(
 4522                lambda: self._parse_csv(
 4523                    lambda: self._parse_hint_function_call() or self._parse_var(upper=True),
 4524                ),
 4525                [],
 4526            ):
 4527                hints.extend(hint)
 4528        except ParseError:
 4529            should_fallback_to_string = True
 4530
 4531        if should_fallback_to_string or self._curr:
 4532            self._retreat(start_index)
 4533            return self._parse_hint_fallback_to_string()
 4534
 4535        return self.expression(exp.Hint(expressions=hints))
 4536
 4537    def _parse_hint(self) -> exp.Hint | None:
 4538        if self._match(TokenType.HINT) and self._prev_comments:
 4539            return exp.maybe_parse(self._prev_comments[0], into=exp.Hint, dialect=self.dialect)
 4540
 4541        return None
 4542
 4543    def _parse_into(self) -> exp.Into | None:
 4544        if not self._match(TokenType.INTO):
 4545            return None
 4546
 4547        temp = self._match(TokenType.TEMPORARY)
 4548        unlogged = self._match_text_seq("UNLOGGED")
 4549        self._match(TokenType.TABLE)
 4550
 4551        return self.expression(
 4552            exp.Into(this=self._parse_table(schema=True), temporary=temp, unlogged=unlogged)
 4553        )
 4554
 4555    def _parse_from(
 4556        self,
 4557        joins: bool = False,
 4558        skip_from_token: bool = False,
 4559        consume_pipe: bool = False,
 4560    ) -> exp.From | None:
 4561        if not skip_from_token and not self._match(TokenType.FROM):
 4562            return None
 4563
 4564        comments = self._prev_comments
 4565        return self.expression(
 4566            exp.From(this=self._parse_table(joins=joins, consume_pipe=consume_pipe)),
 4567            comments=comments,
 4568        )
 4569
 4570    def _parse_match_recognize_measure(self) -> exp.MatchRecognizeMeasure:
 4571        return self.expression(
 4572            exp.MatchRecognizeMeasure(
 4573                window_frame=self._match_texts(("FINAL", "RUNNING")) and self._prev.text.upper(),
 4574                this=self._parse_expression(),
 4575            )
 4576        )
 4577
 4578    def _parse_match_recognize(self) -> exp.MatchRecognize | None:
 4579        if not self._match(TokenType.MATCH_RECOGNIZE):
 4580            return None
 4581
 4582        self._match_l_paren()
 4583
 4584        partition = self._parse_partition_by()
 4585        order = self._parse_order()
 4586
 4587        measures = (
 4588            self._parse_csv(self._parse_match_recognize_measure)
 4589            if self._match_text_seq("MEASURES")
 4590            else None
 4591        )
 4592
 4593        if self._match_text_seq("ONE", "ROW", "PER", "MATCH"):
 4594            rows = exp.var("ONE ROW PER MATCH")
 4595        elif self._match_text_seq("ALL", "ROWS", "PER", "MATCH"):
 4596            text = "ALL ROWS PER MATCH"
 4597            if self._match_text_seq("SHOW", "EMPTY", "MATCHES"):
 4598                text += " SHOW EMPTY MATCHES"
 4599            elif self._match_text_seq("OMIT", "EMPTY", "MATCHES"):
 4600                text += " OMIT EMPTY MATCHES"
 4601            elif self._match_text_seq("WITH", "UNMATCHED", "ROWS"):
 4602                text += " WITH UNMATCHED ROWS"
 4603            rows = exp.var(text)
 4604        else:
 4605            rows = None
 4606
 4607        if self._match_text_seq("AFTER", "MATCH", "SKIP"):
 4608            text = "AFTER MATCH SKIP"
 4609            if self._match_text_seq("PAST", "LAST", "ROW"):
 4610                text += " PAST LAST ROW"
 4611            elif self._match_text_seq("TO", "NEXT", "ROW"):
 4612                text += " TO NEXT ROW"
 4613            elif self._match_text_seq("TO", "FIRST") or self._match_text_seq("TO", "LAST"):
 4614                direction = self._prev.text.upper()
 4615                pattern_var = self._advance_any()
 4616                if not pattern_var:
 4617                    self.raise_error(
 4618                        f"Expecting pattern variable after AFTER MATCH SKIP TO {direction}"
 4619                    )
 4620                text += f" TO {direction} {pattern_var.text if pattern_var else ''}"
 4621            after = exp.var(text)
 4622        else:
 4623            after = None
 4624
 4625        if self._match_text_seq("PATTERN"):
 4626            self._match_l_paren()
 4627
 4628            if not self._curr:
 4629                self.raise_error("Expecting )", self._curr)
 4630
 4631            paren = 1
 4632            start = self._curr
 4633
 4634            while self._curr and paren > 0:
 4635                if self._curr.token_type == TokenType.L_PAREN:
 4636                    paren += 1
 4637                if self._curr.token_type == TokenType.R_PAREN:
 4638                    paren -= 1
 4639
 4640                end = self._prev
 4641                self._advance()
 4642
 4643            if paren > 0:
 4644                self.raise_error("Expecting )", self._curr)
 4645
 4646            pattern = exp.var(self._find_sql(start, end))
 4647        else:
 4648            pattern = None
 4649
 4650        define = (
 4651            self._parse_csv(self._parse_name_as_expression)
 4652            if self._match_text_seq("DEFINE")
 4653            else None
 4654        )
 4655
 4656        self._match_r_paren()
 4657
 4658        return self.expression(
 4659            exp.MatchRecognize(
 4660                partition_by=partition,
 4661                order=order,
 4662                measures=measures,
 4663                rows=rows,
 4664                after=after,
 4665                pattern=pattern,
 4666                define=define,
 4667                alias=self._parse_table_alias(),
 4668            )
 4669        )
 4670
 4671    def _parse_lateral(self) -> exp.Lateral | None:
 4672        cross_apply: bool | None = None
 4673        if self._match_pair(TokenType.CROSS, TokenType.APPLY):
 4674            cross_apply = True
 4675        elif self._match_pair(TokenType.OUTER, TokenType.APPLY):
 4676            cross_apply = False
 4677
 4678        if cross_apply is not None:
 4679            this = self._parse_select(table=True)
 4680            view = None
 4681            outer = None
 4682        elif self._match(TokenType.LATERAL):
 4683            this = self._parse_select(table=True)
 4684            view = self._match(TokenType.VIEW)
 4685            outer = self._match(TokenType.OUTER)
 4686        else:
 4687            return None
 4688
 4689        if not this:
 4690            this = (
 4691                self._parse_unnest()
 4692                or self._parse_function()
 4693                or self._parse_id_var(any_token=False)
 4694            )
 4695
 4696            while self._match(TokenType.DOT):
 4697                this = exp.Dot(
 4698                    this=this,
 4699                    expression=self._parse_function() or self._parse_id_var(any_token=False),
 4700                )
 4701
 4702        ordinality: bool | None = None
 4703
 4704        if view:
 4705            table = self._parse_id_var(any_token=False)
 4706            columns = self._parse_csv(self._parse_id_var) if self._match(TokenType.ALIAS) else []
 4707            table_alias: exp.TableAlias | None = self.expression(
 4708                exp.TableAlias(this=table, columns=columns)
 4709            )
 4710        elif isinstance(this, (exp.Subquery, exp.Unnest)) and this.alias:
 4711            # We move the alias from the lateral's child node to the lateral itself
 4712            table_alias = this.args["alias"].pop()
 4713        else:
 4714            ordinality = self._match_pair(TokenType.WITH, TokenType.ORDINALITY)
 4715            table_alias = self._parse_table_alias()
 4716
 4717        return self.expression(
 4718            exp.Lateral(
 4719                this=this,
 4720                view=view,
 4721                outer=outer,
 4722                alias=table_alias,
 4723                cross_apply=cross_apply,
 4724                ordinality=ordinality,
 4725            )
 4726        )
 4727
 4728    def _parse_stream(self) -> exp.Stream | None:
 4729        index = self._index
 4730        if self._match(TokenType.STREAM):
 4731            if this := self._try_parse(self._parse_table):
 4732                return self.expression(exp.Stream(this=this))
 4733            self._retreat(index)
 4734        return None
 4735
 4736    def _parse_join_parts(
 4737        self,
 4738    ) -> tuple[Token | None, Token | None, Token | None]:
 4739        return (
 4740            self._prev if self._match_set(self.JOIN_METHODS) else None,
 4741            self._prev if self._match_set(self.JOIN_SIDES) else None,
 4742            self._prev if self._match_set(self.JOIN_KINDS) else None,
 4743        )
 4744
 4745    def _parse_using_identifiers(self) -> list[exp.Expr]:
 4746        def _parse_column_as_identifier() -> exp.Expr | None:
 4747            this = self._parse_column()
 4748            if isinstance(this, exp.Column):
 4749                return this.this
 4750            return this
 4751
 4752        return self._parse_wrapped_csv(_parse_column_as_identifier, optional=True)
 4753
 4754    def _parse_join(
 4755        self,
 4756        skip_join_token: bool = False,
 4757        parse_bracket: bool = False,
 4758        alias_tokens: t.Collection[TokenType] | None = None,
 4759    ) -> exp.Join | None:
 4760        if self._match(TokenType.COMMA):
 4761            table = self._try_parse(lambda: self._parse_table(alias_tokens=alias_tokens))
 4762            cross_join = self.expression(exp.Join(this=table)) if table else None
 4763
 4764            if cross_join and self.JOINS_HAVE_EQUAL_PRECEDENCE:
 4765                cross_join.set("kind", "CROSS")
 4766
 4767            return cross_join
 4768
 4769        index = self._index
 4770        method, side, kind = self._parse_join_parts()
 4771        directed = self._match_text_seq("DIRECTED")
 4772        hint = self._prev.text if self._match_texts(self.JOIN_HINTS) else None
 4773        join = self._match(TokenType.JOIN) or (kind and kind.token_type == TokenType.STRAIGHT_JOIN)
 4774        join_comments = self._prev_comments
 4775
 4776        if not skip_join_token and not join:
 4777            self._retreat(index)
 4778            kind = None
 4779            method = None
 4780            side = None
 4781
 4782        outer_apply = self._match_pair(TokenType.OUTER, TokenType.APPLY, False)
 4783        cross_apply = self._match_pair(TokenType.CROSS, TokenType.APPLY, False)
 4784
 4785        if not skip_join_token and not join and not outer_apply and not cross_apply:
 4786            return None
 4787
 4788        kwargs: dict[str, t.Any] = {
 4789            "this": self._parse_table(parse_bracket=parse_bracket, alias_tokens=alias_tokens)
 4790        }
 4791        if kind and kind.token_type == TokenType.ARRAY and self._match(TokenType.COMMA):
 4792            kwargs["expressions"] = self._parse_csv(
 4793                lambda: self._parse_table(parse_bracket=parse_bracket, alias_tokens=alias_tokens)
 4794            )
 4795
 4796        if method:
 4797            kwargs["method"] = method.text.upper()
 4798        if side:
 4799            kwargs["side"] = side.text.upper()
 4800        if kind:
 4801            kwargs["kind"] = kind.text.upper()
 4802        if hint:
 4803            kwargs["hint"] = hint
 4804
 4805        if self._match(TokenType.MATCH_CONDITION):
 4806            kwargs["match_condition"] = self._parse_wrapped(self._parse_comparison)
 4807
 4808        if self._match(TokenType.ON):
 4809            kwargs["on"] = self._parse_disjunction()
 4810        elif self._match(TokenType.USING):
 4811            kwargs["using"] = self._parse_using_identifiers()
 4812        elif (
 4813            not method
 4814            and not (outer_apply or cross_apply)
 4815            and not isinstance(kwargs["this"], exp.Unnest)
 4816            and not (kind and kind.token_type in (TokenType.CROSS, TokenType.ARRAY))
 4817        ):
 4818            index = self._index
 4819            joins: list | None = list(self._parse_joins(alias_tokens=alias_tokens))
 4820
 4821            if joins and self._match(TokenType.ON):
 4822                kwargs["on"] = self._parse_disjunction()
 4823            elif joins and self._match(TokenType.USING):
 4824                kwargs["using"] = self._parse_using_identifiers()
 4825            else:
 4826                joins = None
 4827                self._retreat(index)
 4828
 4829            kwargs["this"].set("joins", joins if joins else None)
 4830
 4831        kwargs["pivots"] = self._parse_pivots()
 4832
 4833        comments = [c for token in (method, side, kind) if token for c in token.comments]
 4834        comments = (join_comments or []) + comments
 4835
 4836        if (
 4837            self.ADD_JOIN_ON_TRUE
 4838            and not kwargs.get("on")
 4839            and not kwargs.get("using")
 4840            and not kwargs.get("method")
 4841            and kwargs.get("kind") in (None, "INNER", "OUTER")
 4842        ):
 4843            kwargs["on"] = exp.true()
 4844
 4845        if directed:
 4846            kwargs["directed"] = directed
 4847
 4848        return self.expression(exp.Join(**kwargs), comments=comments)
 4849
 4850    def _parse_opclass(self) -> exp.Expr | None:
 4851        this = self._parse_disjunction()
 4852
 4853        if self._match_texts(self.OPCLASS_FOLLOW_KEYWORDS, advance=False):
 4854            return this
 4855
 4856        if not self._match_set(self.OPTYPE_FOLLOW_TOKENS, advance=False):
 4857            return self.expression(exp.Opclass(this=this, expression=self._parse_table_parts()))
 4858
 4859        return this
 4860
 4861    def _parse_index_params(self) -> exp.IndexParameters:
 4862        using = self._parse_var(any_token=True) if self._match(TokenType.USING) else None
 4863
 4864        if self._match(TokenType.L_PAREN, advance=False):
 4865            columns = self._parse_wrapped_csv(self._parse_with_operator)
 4866        else:
 4867            columns = None
 4868
 4869        include = self._parse_wrapped_id_vars() if self._match_text_seq("INCLUDE") else None
 4870        partition_by = self._parse_partition_by()
 4871        with_storage = self._match(TokenType.WITH) and self._parse_wrapped_properties()
 4872        tablespace = (
 4873            self._parse_var(any_token=True)
 4874            if self._match_text_seq("USING", "INDEX", "TABLESPACE")
 4875            else None
 4876        )
 4877        where = self._parse_where()
 4878
 4879        on = self._parse_field() if self._match(TokenType.ON) else None
 4880
 4881        return self.expression(
 4882            exp.IndexParameters(
 4883                using=using,
 4884                columns=columns,
 4885                include=include,
 4886                partition_by=partition_by,
 4887                where=where,
 4888                with_storage=with_storage,
 4889                tablespace=tablespace,
 4890                on=on,
 4891            )
 4892        )
 4893
 4894    def _parse_index(
 4895        self, index: exp.Expr | None = None, anonymous: bool = False
 4896    ) -> exp.Index | None:
 4897        if index or anonymous:
 4898            unique = None
 4899            primary = None
 4900            amp = None
 4901
 4902            self._match(TokenType.ON)
 4903            self._match(TokenType.TABLE)  # hive
 4904            table = self._parse_table_parts(schema=True)
 4905        else:
 4906            unique = self._match(TokenType.UNIQUE)
 4907            primary = self._match_text_seq("PRIMARY")
 4908            amp = self._match_text_seq("AMP")
 4909
 4910            if not self._match(TokenType.INDEX):
 4911                return None
 4912
 4913            index = self._parse_id_var()
 4914            table = None
 4915
 4916        params = self._parse_index_params()
 4917
 4918        return self.expression(
 4919            exp.Index(
 4920                this=index, table=table, unique=unique, primary=primary, amp=amp, params=params
 4921            )
 4922        )
 4923
 4924    def _parse_table_hints(self) -> list[exp.Expr] | None:
 4925        hints: list[exp.Expr] = []
 4926        if self._match_pair(TokenType.WITH, TokenType.L_PAREN):
 4927            # https://learn.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-table?view=sql-server-ver16
 4928            hints.append(
 4929                self.expression(
 4930                    exp.WithTableHint(
 4931                        expressions=self._parse_csv(
 4932                            lambda: self._parse_function() or self._parse_var(any_token=True)
 4933                        )
 4934                    )
 4935                )
 4936            )
 4937            self._match_r_paren()
 4938        else:
 4939            # https://dev.mysql.com/doc/refman/8.0/en/index-hints.html
 4940            while self._match_set(self.TABLE_INDEX_HINT_TOKENS):
 4941                hint = exp.IndexTableHint(this=self._prev.text.upper())
 4942
 4943                self._match_set((TokenType.INDEX, TokenType.KEY))
 4944                if self._match(TokenType.FOR):
 4945                    hint.set("target", self._advance_any() and self._prev.text.upper())
 4946
 4947                hint.set("expressions", self._parse_wrapped_id_vars())
 4948                hints.append(hint)
 4949
 4950        return hints or None
 4951
 4952    def _parse_table_part(self, schema: bool = False) -> exp.Expr | None:
 4953        return (
 4954            (not schema and self._parse_function(optional_parens=False))
 4955            or self._parse_id_var(any_token=False)
 4956            or self._parse_string_as_identifier()
 4957            or self._parse_placeholder()
 4958        )
 4959
 4960    def _parse_table_parts_fast(self) -> exp.Table | None:
 4961        index = self._index
 4962        parts: list[exp.Identifier] | None = None
 4963        all_comments: list[str] | None = None
 4964
 4965        while self._match_set(self.IDENTIFIER_TOKENS):
 4966            token = self._prev
 4967            comments = self._prev_comments
 4968
 4969            has_dot = self._match(TokenType.DOT)
 4970            curr_tt = self._curr.token_type
 4971
 4972            if not has_dot:
 4973                if curr_tt in self.TABLE_POSTFIX_TOKENS:
 4974                    self._retreat(index)
 4975                    return None
 4976            elif curr_tt not in self.IDENTIFIER_TOKENS:
 4977                self._retreat(index)
 4978                return None
 4979
 4980            if parts is None:
 4981                parts = []
 4982
 4983            if comments:
 4984                if all_comments is None:
 4985                    all_comments = []
 4986                all_comments.extend(comments)
 4987                self._prev_comments = []
 4988
 4989            parts.append(
 4990                self.expression(
 4991                    exp.Identifier(
 4992                        this=token.text, quoted=token.token_type == TokenType.IDENTIFIER
 4993                    ),
 4994                    token,
 4995                )
 4996            )
 4997
 4998            if not has_dot:
 4999                break
 5000
 5001        if parts is None:
 5002            return None
 5003
 5004        n = len(parts)
 5005
 5006        if n == 1:
 5007            table: exp.Table = exp.Table(this=parts[0])
 5008        elif n == 2:
 5009            table = exp.Table(this=parts[1], db=parts[0])
 5010        elif n >= 3:
 5011            this: exp.Identifier | exp.Dot = parts[2]
 5012            for i in range(3, n):
 5013                this = exp.Dot(this=this, expression=parts[i])
 5014
 5015            table = exp.Table(this=this, db=parts[1], catalog=parts[0])
 5016
 5017        if table is None:
 5018            self._retreat(index)
 5019        elif all_comments:
 5020            table.add_comments(all_comments)
 5021        return table
 5022
 5023    def _parse_table_parts(
 5024        self,
 5025        schema: bool = False,
 5026        is_db_reference: bool = False,
 5027        wildcard: bool = False,
 5028        fast: bool = False,
 5029    ) -> exp.Table | exp.Dot | None:
 5030        if fast:
 5031            return self._parse_table_parts_fast()
 5032
 5033        catalog: exp.Expr | str | None = None
 5034        db: exp.Expr | str | None = None
 5035        table: exp.Expr | str | None = self._parse_table_part(schema=schema)
 5036
 5037        while self._match(TokenType.DOT):
 5038            if catalog:
 5039                # This allows nesting the table in arbitrarily many dot expressions if needed
 5040                table = self.expression(
 5041                    exp.Dot(this=table, expression=self._parse_table_part(schema=schema))
 5042                )
 5043            else:
 5044                catalog = db
 5045                db = table
 5046                # "" used for tsql FROM a..b case
 5047                table = self._parse_table_part(schema=schema) or ""
 5048
 5049        if (
 5050            wildcard
 5051            and self._is_connected()
 5052            and (isinstance(table, exp.Identifier) or not table)
 5053            and self._match(TokenType.STAR)
 5054        ):
 5055            if isinstance(table, exp.Identifier):
 5056                table.args["this"] += "*"
 5057            else:
 5058                table = exp.Identifier(this="*")
 5059
 5060        if is_db_reference:
 5061            catalog = db
 5062            db = table
 5063            table = None
 5064
 5065        if not table and not is_db_reference:
 5066            self.raise_error(f"Expected table name but got {self._curr}")
 5067        if not db and is_db_reference:
 5068            self.raise_error(f"Expected database name but got {self._curr}")
 5069
 5070        table = self.expression(exp.Table(this=table, db=db, catalog=catalog))
 5071
 5072        # Bubble up comments from identifier parts to the Table
 5073        comments = []
 5074        for part in table.parts:
 5075            if part_comments := part.pop_comments():
 5076                comments.extend(part_comments)
 5077        if comments:
 5078            table.add_comments(comments)
 5079
 5080        changes = self._parse_changes()
 5081        if changes:
 5082            table.set("changes", changes)
 5083
 5084        at_before = self._parse_historical_data()
 5085        if at_before:
 5086            table.set("when", at_before)
 5087
 5088        pivots = self._parse_pivots()
 5089        if pivots:
 5090            table.set("pivots", pivots)
 5091
 5092        return table
 5093
 5094    def _parse_table(
 5095        self,
 5096        schema: bool = False,
 5097        joins: bool = False,
 5098        alias_tokens: t.Collection[TokenType] | None = None,
 5099        parse_bracket: bool = False,
 5100        is_db_reference: bool = False,
 5101        parse_partition: bool = False,
 5102        consume_pipe: bool = False,
 5103    ) -> exp.Expr | None:
 5104        if not schema and not is_db_reference and not consume_pipe and not joins:
 5105            index = self._index
 5106            table = self._parse_table_parts(fast=True)
 5107
 5108            if table is not None:
 5109                curr_tt = self._curr.token_type
 5110                next_tt = self._next.token_type
 5111
 5112                fast_terminators = self.TABLE_TERMINATORS
 5113
 5114                # only return the table if we're sure there are no other operators
 5115                # MATCH_CONDITION is a special case because it accepts any alias before it like LIMIT
 5116                if curr_tt in fast_terminators and next_tt != TokenType.MATCH_CONDITION:
 5117                    return table
 5118
 5119                postfix_tokens = self.TABLE_POSTFIX_TOKENS
 5120
 5121                if curr_tt not in postfix_tokens and next_tt not in postfix_tokens:
 5122                    if alias := self._parse_table_alias(
 5123                        alias_tokens=alias_tokens or self.TABLE_ALIAS_TOKENS
 5124                    ):
 5125                        table.set("alias", alias)
 5126
 5127                    if self._curr.token_type in fast_terminators:
 5128                        return table
 5129
 5130                self._retreat(index)
 5131
 5132        if stream := self._parse_stream():
 5133            return stream
 5134
 5135        if lateral := self._parse_lateral():
 5136            return lateral
 5137
 5138        if unnest := self._parse_unnest():
 5139            return unnest
 5140
 5141        if values := self._parse_derived_table_values():
 5142            return values
 5143
 5144        if subquery := self._parse_select(table=True, consume_pipe=consume_pipe):
 5145            if not subquery.args.get("pivots"):
 5146                subquery.set("pivots", self._parse_pivots())
 5147            if joins:
 5148                for join in self._parse_joins():
 5149                    subquery.append("joins", join)
 5150            return subquery
 5151
 5152        bracket = parse_bracket and self._parse_bracket(None)
 5153        bracket = self.expression(exp.Table(this=bracket)) if bracket else None
 5154
 5155        rows_from_tables = (
 5156            self._parse_wrapped_csv(self._parse_table)
 5157            if self._match_text_seq("ROWS", "FROM")
 5158            else None
 5159        )
 5160        rows_from = (
 5161            self.expression(exp.Table(rows_from=rows_from_tables)) if rows_from_tables else None
 5162        )
 5163
 5164        only = self._match(TokenType.ONLY)
 5165
 5166        this = t.cast(
 5167            exp.Expr,
 5168            bracket
 5169            or rows_from
 5170            or self._parse_bracket(
 5171                self._parse_table_parts(schema=schema, is_db_reference=is_db_reference)
 5172            ),
 5173        )
 5174
 5175        if only:
 5176            this.set("only", only)
 5177
 5178        # Postgres supports a wildcard (table) suffix operator, which is a no-op in this context
 5179        self._match(TokenType.STAR)
 5180
 5181        parse_partition = parse_partition or self.SUPPORTS_PARTITION_SELECTION
 5182        if parse_partition and self._match(TokenType.PARTITION, advance=False):
 5183            this.set("partition", self._parse_partition())
 5184
 5185        if schema:
 5186            return self._parse_schema(this=this)
 5187
 5188        if self.dialect.ALIAS_POST_VERSION:
 5189            this.set("version", self._parse_version())
 5190
 5191        if self.dialect.ALIAS_POST_TABLESAMPLE:
 5192            this.set("sample", self._parse_table_sample())
 5193
 5194        alias = self._parse_table_alias(alias_tokens=alias_tokens or self.TABLE_ALIAS_TOKENS)
 5195        if alias:
 5196            this.set("alias", alias)
 5197
 5198            # DuckDB requires the time-travel clause to come after the alias, e.g.
 5199            # SELECT * FROM t AS a AT (VERSION => 1)
 5200            if isinstance(this, exp.Table) and not this.args.get("when"):
 5201                this.set("when", self._parse_historical_data())
 5202
 5203        if self._match(TokenType.INDEXED_BY):
 5204            this.set("indexed", self._parse_table_parts())
 5205        elif self._match_text_seq("NOT", "INDEXED"):
 5206            this.set("indexed", False)
 5207
 5208        if isinstance(this, exp.Table) and self._match_text_seq("AT"):
 5209            return self.expression(
 5210                exp.AtIndex(this=this.to_column(copy=False), expression=self._parse_id_var())
 5211            )
 5212
 5213        this.set("hints", self._parse_table_hints())
 5214
 5215        if not this.args.get("pivots"):
 5216            this.set("pivots", self._parse_pivots())
 5217
 5218        if not self.dialect.ALIAS_POST_TABLESAMPLE:
 5219            this.set("sample", self._parse_table_sample())
 5220
 5221        if not self.dialect.ALIAS_POST_VERSION:
 5222            this.set("version", self._parse_version())
 5223
 5224        if joins:
 5225            for join in self._parse_joins(alias_tokens=alias_tokens):
 5226                this.append("joins", join)
 5227
 5228        if self._match_pair(TokenType.WITH, TokenType.ORDINALITY):
 5229            this.set("ordinality", True)
 5230            this.set("alias", self._parse_table_alias())
 5231
 5232        # TABLE(<tvf>) is parsed into a Table wrapping exp.TableFromRows, so we
 5233        # hoist the table args onto the latter and return it instead
 5234        if isinstance(this, exp.Table) and isinstance(this.this, exp.TableFromRows):
 5235            table_from_rows = this.this
 5236            for arg in exp.TableFromRows.arg_types:
 5237                if arg != "this":
 5238                    table_from_rows.set(arg, this.args.get(arg))
 5239
 5240            this = table_from_rows
 5241
 5242        return this
 5243
 5244    def _parse_version(self) -> exp.Version | None:
 5245        for phrase, this in self.VERSION_PHRASES.items():
 5246            if self._match_text_seq(*phrase):
 5247                break
 5248        else:
 5249            return None
 5250
 5251        if self._match_set((TokenType.FROM, TokenType.BETWEEN)):
 5252            kind = self._prev.text.upper()
 5253            start = self._parse_bitwise()
 5254            self._match_texts(("TO", "AND"))
 5255            end = self._parse_bitwise()
 5256            expression: exp.Expr | None = self.expression(exp.Tuple(expressions=[start, end]))
 5257        elif self._match_text_seq("CONTAINED", "IN"):
 5258            kind = "CONTAINED IN"
 5259            expression = self.expression(
 5260                exp.Tuple(expressions=self._parse_wrapped_csv(self._parse_bitwise))
 5261            )
 5262        elif self._match(TokenType.ALL):
 5263            kind = "ALL"
 5264            expression = None
 5265        else:
 5266            self._match_text_seq("AS", "OF")
 5267            kind = "AS OF"
 5268            expression = self._parse_type()
 5269
 5270        return self.expression(exp.Version(this=this, expression=expression, kind=kind))
 5271
 5272    def _parse_historical_data(self) -> exp.HistoricalData | None:
 5273        # https://docs.snowflake.com/en/sql-reference/constructs/at-before
 5274        index = self._index
 5275        historical_data = None
 5276        if self._match_texts(self.HISTORICAL_DATA_PREFIX):
 5277            this = self._prev.text.upper()
 5278            kind = (
 5279                self._match(TokenType.L_PAREN)
 5280                and self._match_texts(self.HISTORICAL_DATA_KIND)
 5281                and self._prev.text.upper()
 5282            )
 5283            expression = self._match(TokenType.FARROW) and self._parse_bitwise()
 5284
 5285            if expression:
 5286                self._match_r_paren()
 5287                historical_data = self.expression(
 5288                    exp.HistoricalData(this=this, kind=kind, expression=expression)
 5289                )
 5290            else:
 5291                self._retreat(index)
 5292
 5293        return historical_data
 5294
 5295    def _parse_changes(self) -> exp.Changes | None:
 5296        if not self._match_text_seq("CHANGES", "(", "INFORMATION", "=>"):
 5297            return None
 5298
 5299        information = self._parse_var(any_token=True)
 5300        self._match_r_paren()
 5301
 5302        return self.expression(
 5303            exp.Changes(
 5304                information=information,
 5305                at_before=self._parse_historical_data(),
 5306                end=self._parse_historical_data(),
 5307            )
 5308        )
 5309
 5310    def _parse_unnest(self, with_alias: bool = True) -> exp.Unnest | None:
 5311        if not self._match_pair(TokenType.UNNEST, TokenType.L_PAREN, advance=False):
 5312            return None
 5313
 5314        self._advance()
 5315
 5316        expressions = self._parse_wrapped_csv(self._parse_equality)
 5317        offset: bool | exp.Expr = self._match_pair(TokenType.WITH, TokenType.ORDINALITY)
 5318
 5319        alias = self._parse_table_alias() if with_alias else None
 5320
 5321        if alias:
 5322            if self.dialect.UNNEST_COLUMN_ONLY:
 5323                if alias.args.get("columns"):
 5324                    self.raise_error("Unexpected extra column alias in unnest.")
 5325
 5326                alias.set("columns", [alias.this])
 5327                alias.set("this", None)
 5328
 5329            columns = alias.args.get("columns") or []
 5330            if offset and len(expressions) < len(columns):
 5331                offset = columns.pop()
 5332
 5333        if not offset and self._match_pair(TokenType.WITH, TokenType.OFFSET):
 5334            self._match(TokenType.ALIAS)
 5335            offset = self._parse_id_var(
 5336                any_token=False, tokens=self.UNNEST_OFFSET_ALIAS_TOKENS
 5337            ) or exp.to_identifier("offset")
 5338
 5339        return self.expression(exp.Unnest(expressions=expressions, alias=alias, offset=offset))
 5340
 5341    def _parse_derived_table_values(self, allow_value_synonym: bool = False) -> exp.Values | None:
 5342        is_derived = self._match_pair(TokenType.L_PAREN, TokenType.VALUES)
 5343        if not is_derived and not (
 5344            # ClickHouse's `FORMAT Values` is equivalent to `VALUES`
 5345            self._match_text_seq("VALUES")
 5346            or self._match_text_seq("FORMAT", "VALUES")
 5347            # MySQL accepts VALUE as a synonym for VALUES in INSERT statements.
 5348            or (allow_value_synonym and self._match_text_seq("VALUE"))
 5349        ):
 5350            return None
 5351
 5352        expressions = self._parse_csv(self._parse_value)
 5353        alias = self._parse_table_alias()
 5354
 5355        if is_derived:
 5356            self._match_r_paren()
 5357
 5358        return self.expression(
 5359            exp.Values(expressions=expressions, alias=alias or self._parse_table_alias())
 5360        )
 5361
 5362    def _parse_table_sample(self, as_modifier: bool = False) -> exp.TableSample | None:
 5363        if not self._match(TokenType.TABLE_SAMPLE) and not (
 5364            as_modifier and self._match_text_seq("USING", "SAMPLE")
 5365        ):
 5366            return None
 5367
 5368        bucket_numerator = None
 5369        bucket_denominator = None
 5370        bucket_field = None
 5371        percent = None
 5372        size = None
 5373        seed = None
 5374
 5375        method = self._parse_var(tokens=(TokenType.ROW,), upper=True)
 5376        matched_l_paren = self._match(TokenType.L_PAREN)
 5377
 5378        if self.TABLESAMPLE_CSV:
 5379            num = None
 5380            expressions = self._parse_csv(self._parse_primary)
 5381        else:
 5382            expressions = None
 5383            num = (
 5384                self._parse_factor(parse_mod=False)
 5385                if self._match(TokenType.NUMBER, advance=False)
 5386                else self._parse_primary() or self._parse_placeholder()
 5387            )
 5388
 5389        if self._match_text_seq("BUCKET"):
 5390            bucket_numerator = self._parse_number()
 5391            self._match_text_seq("OUT", "OF")
 5392            bucket_denominator = bucket_denominator = self._parse_number()
 5393            self._match(TokenType.ON)
 5394            bucket_field = self._parse_field()
 5395        elif self._match_set((TokenType.PERCENT, TokenType.MOD)):
 5396            percent = num
 5397        elif self._match(TokenType.ROWS) or not self.dialect.TABLESAMPLE_SIZE_IS_PERCENT:
 5398            size = num
 5399        else:
 5400            percent = num
 5401
 5402        if matched_l_paren:
 5403            self._match_r_paren()
 5404
 5405        if self._match(TokenType.L_PAREN):
 5406            method = self._parse_var(upper=True)
 5407            seed = self._match(TokenType.COMMA) and self._parse_number()
 5408            self._match_r_paren()
 5409        elif self._match_texts(("SEED", "REPEATABLE")):
 5410            seed = self._parse_wrapped(self._parse_number)
 5411
 5412        if not method and self.DEFAULT_SAMPLING_METHOD:
 5413            method = exp.var(self.DEFAULT_SAMPLING_METHOD)
 5414
 5415        return self.expression(
 5416            exp.TableSample(
 5417                expressions=expressions,
 5418                method=method,
 5419                bucket_numerator=bucket_numerator,
 5420                bucket_denominator=bucket_denominator,
 5421                bucket_field=bucket_field,
 5422                percent=percent,
 5423                size=size,
 5424                seed=seed,
 5425            )
 5426        )
 5427
 5428    def _parse_pivots(self) -> list[exp.Pivot] | None:
 5429        if self._curr.token_type not in (TokenType.PIVOT, TokenType.UNPIVOT):
 5430            return None
 5431        return list(iter(self._parse_pivot, None)) or None
 5432
 5433    def _parse_joins(
 5434        self, alias_tokens: t.Collection[TokenType] | None = None
 5435    ) -> t.Iterator[exp.Join]:
 5436        return iter(lambda: self._parse_join(alias_tokens=alias_tokens), None)
 5437
 5438    def _parse_unpivot_columns(self) -> exp.UnpivotColumns | None:
 5439        if not self._match(TokenType.INTO):
 5440            return None
 5441
 5442        return self.expression(
 5443            exp.UnpivotColumns(
 5444                this=self._match_text_seq("NAME") and self._parse_column(),
 5445                expressions=self._match_text_seq("VALUE") and self._parse_csv(self._parse_column),
 5446            )
 5447        )
 5448
 5449    # https://duckdb.org/docs/sql/statements/pivot
 5450    def _parse_simplified_pivot(self, is_unpivot: bool | None = None) -> exp.Pivot:
 5451        def _parse_on() -> exp.Expr | None:
 5452            this = self._parse_bitwise()
 5453
 5454            if self._match(TokenType.IN):
 5455                # PIVOT ... ON col IN (row_val1, row_val2)
 5456                return self._parse_in(this)
 5457            if self._match(TokenType.ALIAS, advance=False):
 5458                # UNPIVOT ... ON (col1, col2, col3) AS row_val
 5459                return self._parse_alias(this)
 5460
 5461            return this
 5462
 5463        this = self._parse_table()
 5464        expressions = self._match(TokenType.ON) and self._parse_csv(_parse_on)
 5465        into = self._parse_unpivot_columns()
 5466        using = self._match(TokenType.USING) and self._parse_csv(
 5467            lambda: self._parse_alias(self._parse_column())
 5468        )
 5469        group = self._parse_group()
 5470
 5471        return self.expression(
 5472            exp.Pivot(
 5473                this=this,
 5474                expressions=expressions,
 5475                using=using,
 5476                group=group,
 5477                unpivot=is_unpivot,
 5478                into=into,
 5479            )
 5480        )
 5481
 5482    def _parse_pivot_in(self) -> exp.In:
 5483        def _parse_aliased_expression() -> exp.Expr | None:
 5484            this = self._parse_select_or_expression()
 5485
 5486            self._match(TokenType.ALIAS)
 5487            alias = self._parse_bitwise()
 5488            if alias:
 5489                if isinstance(alias, exp.Column) and not alias.db:
 5490                    alias = alias.this
 5491                return self.expression(exp.PivotAlias(this=this, alias=alias))
 5492
 5493            return this
 5494
 5495        value = self._parse_column()
 5496
 5497        if not self._match(TokenType.IN):
 5498            self.raise_error("Expecting IN")
 5499
 5500        if self._match(TokenType.L_PAREN):
 5501            if self._match(TokenType.ANY):
 5502                exprs: list[exp.Expr] = ensure_list(exp.PivotAny(this=self._parse_order()))
 5503            else:
 5504                exprs = self._parse_csv(_parse_aliased_expression)
 5505            self._match_r_paren()
 5506            return self.expression(exp.In(this=value, expressions=exprs))
 5507
 5508        return self.expression(exp.In(this=value, field=self._parse_id_var()))
 5509
 5510    def _parse_pivot_aggregation(self) -> exp.Expr | None:
 5511        func = self._parse_function()
 5512        if not func:
 5513            if self._prev.token_type == TokenType.COMMA:
 5514                return None
 5515            self.raise_error("Expecting an aggregation function in PIVOT")
 5516
 5517        return self._parse_alias(func)
 5518
 5519    def _parse_pivot(self) -> exp.Pivot | None:
 5520        index = self._index
 5521        include_nulls = None
 5522
 5523        if self._match(TokenType.PIVOT):
 5524            unpivot = False
 5525        elif self._match(TokenType.UNPIVOT):
 5526            unpivot = True
 5527
 5528            # https://docs.databricks.com/en/sql/language-manual/sql-ref-syntax-qry-select-unpivot.html#syntax
 5529            if self._match_text_seq("INCLUDE", "NULLS"):
 5530                include_nulls = True
 5531            elif self._match_text_seq("EXCLUDE", "NULLS"):
 5532                include_nulls = False
 5533        else:
 5534            return None
 5535
 5536        expressions = []
 5537
 5538        if not self._match(TokenType.L_PAREN):
 5539            self._retreat(index)
 5540            return None
 5541
 5542        if unpivot:
 5543            expressions = self._parse_csv(self._parse_column)
 5544        else:
 5545            expressions = self._parse_csv(self._parse_pivot_aggregation)
 5546
 5547        if not expressions:
 5548            self.raise_error("Failed to parse PIVOT's aggregation list")
 5549
 5550        if not self._match(TokenType.FOR):
 5551            self.raise_error("Expecting FOR")
 5552
 5553        fields = []
 5554        while True:
 5555            field = self._try_parse(self._parse_pivot_in)
 5556            if not field:
 5557                break
 5558            fields.append(field)
 5559
 5560        default_on_null = self._match_text_seq("DEFAULT", "ON", "NULL") and self._parse_wrapped(
 5561            self._parse_bitwise
 5562        )
 5563
 5564        group = self._parse_group()
 5565
 5566        self._match_r_paren()
 5567
 5568        pivot = self.expression(
 5569            exp.Pivot(
 5570                expressions=expressions,
 5571                fields=fields,
 5572                unpivot=unpivot,
 5573                include_nulls=include_nulls,
 5574                default_on_null=default_on_null,
 5575                group=group,
 5576            )
 5577        )
 5578
 5579        if unpivot:
 5580            pivot.set("expressions", [_unpivot_target(e) for e in pivot.expressions])
 5581            for pivot_field in pivot.fields:
 5582                if isinstance(pivot_field, exp.In):
 5583                    pivot_field.set("this", _unpivot_target(pivot_field.this))
 5584
 5585            pivot.set("value_columns_first", self.UNPIVOT_VALUE_COLUMNS_FIRST)
 5586
 5587        if not self._match_set((TokenType.PIVOT, TokenType.UNPIVOT), advance=False):
 5588            pivot.set("alias", self._parse_table_alias())
 5589
 5590        if not unpivot:
 5591            names = self._pivot_column_names(t.cast(list[exp.Expr], expressions))
 5592
 5593            columns: list[exp.Expr] = []
 5594            all_fields = []
 5595            for pivot_field in pivot.fields:
 5596                pivot_field_expressions = pivot_field.expressions
 5597
 5598                # The `PivotAny` expression corresponds to `ANY ORDER BY <column>`; we can't infer in this case.
 5599                if isinstance(seq_get(pivot_field_expressions, 0), exp.PivotAny):
 5600                    continue
 5601
 5602                all_fields.append(
 5603                    [
 5604                        # An explicit `<field> AS <alias>` names the output column directly,
 5605                        # so it wins over the dialect's string-identifying convention
 5606                        fld.sql()
 5607                        if self.IDENTIFY_PIVOT_STRINGS and not isinstance(fld, exp.PivotAlias)
 5608                        else fld.alias_or_name
 5609                        for fld in pivot_field_expressions
 5610                    ]
 5611                )
 5612
 5613            if all_fields:
 5614                if names:
 5615                    all_fields.append(names)
 5616
 5617                # Generate all possible combinations of the pivot columns
 5618                # e.g PIVOT(sum(...) as total FOR year IN (2000, 2010) FOR country IN ('NL', 'US'))
 5619                # generates the product between [[2000, 2010], ['NL', 'US'], ['total']]
 5620                for fld_parts_tuple in itertools.product(*all_fields):
 5621                    fld_parts = list(fld_parts_tuple)
 5622
 5623                    if names and self.PREFIXED_PIVOT_COLUMNS:
 5624                        # Move the "name" to the front of the list
 5625                        fld_parts.insert(0, fld_parts.pop(-1))
 5626
 5627                    columns.append(exp.to_identifier("_".join(fld_parts)))
 5628
 5629            pivot.set("columns", columns)
 5630            pivot.set("identify_pivot_strings", self.IDENTIFY_PIVOT_STRINGS)
 5631            pivot.set("prefixed_pivot_columns", self.PREFIXED_PIVOT_COLUMNS)
 5632            pivot.set("pivot_column_naming", self.PIVOT_COLUMN_NAMING)
 5633
 5634        return pivot
 5635
 5636    def _pivot_column_names(self, aggregations: list[exp.Expr]) -> list[str]:
 5637        return [agg.alias for agg in aggregations if agg.alias]
 5638
 5639    def _parse_prewhere(self, skip_where_token: bool = False) -> exp.PreWhere | None:
 5640        if not skip_where_token and not self._match(TokenType.PREWHERE):
 5641            return None
 5642
 5643        comments = self._prev_comments
 5644        return self.expression(
 5645            exp.PreWhere(this=self._parse_disjunction()),
 5646            comments=comments,
 5647        )
 5648
 5649    def _parse_where(self, skip_where_token: bool = False) -> exp.Where | None:
 5650        if not skip_where_token and not self._match(TokenType.WHERE):
 5651            return None
 5652
 5653        comments = self._prev_comments
 5654        return self.expression(
 5655            exp.Where(this=self._parse_disjunction()),
 5656            comments=comments,
 5657        )
 5658
 5659    def _parse_group(self, skip_group_by_token: bool = False) -> exp.Group | None:
 5660        if not skip_group_by_token and not self._match(TokenType.GROUP_BY):
 5661            return None
 5662        comments = self._prev_comments
 5663
 5664        elements: dict[str, t.Any] = defaultdict(list)
 5665
 5666        if self._match(TokenType.ALL):
 5667            elements["all"] = True
 5668        elif self._match(TokenType.DISTINCT):
 5669            elements["all"] = False
 5670
 5671        while True:
 5672            # Stop before consuming modifier tokens like LIMIT, OFFSET and WINDOW,
 5673            # which are also valid identifiers
 5674            if self._match_set(self.QUERY_MODIFIER_TOKENS, advance=False):
 5675                break
 5676
 5677            elements["expressions"].extend(
 5678                self._parse_csv(
 5679                    lambda: (
 5680                        self._parse_grouping_sets()
 5681                        or self._parse_cube_or_rollup()
 5682                        or self._parse_disjunction()
 5683                    )
 5684                )
 5685            )
 5686
 5687            before_with_index = self._index
 5688
 5689            if self._match(TokenType.WITH) and (
 5690                cube_or_rollup := self._parse_cube_or_rollup(with_prefix=True)
 5691            ):
 5692                key = "rollup" if isinstance(cube_or_rollup, exp.Rollup) else "cube"
 5693                elements[key].append(cube_or_rollup)
 5694            elif grouping_sets := self._parse_grouping_sets():
 5695                # Hive-style suffix syntax: GROUP BY a, b GROUPING SETS (...)
 5696                elements["grouping_sets"].append(grouping_sets)
 5697                break
 5698            elif self._match_text_seq("TOTALS"):
 5699                elements["totals"] = True  # type: ignore
 5700
 5701            if before_with_index <= self._index <= before_with_index + 1:
 5702                self._retreat(before_with_index)
 5703                break
 5704
 5705        return self.expression(exp.Group(**elements), comments=comments)  # type: ignore
 5706
 5707    def _parse_cube_or_rollup(self, with_prefix: bool = False) -> exp.Cube | exp.Rollup | None:
 5708        if self._match(TokenType.CUBE):
 5709            kind: type[exp.Cube | exp.Rollup] = exp.Cube
 5710        elif self._match(TokenType.ROLLUP):
 5711            kind = exp.Rollup
 5712        else:
 5713            return None
 5714
 5715        return self.expression(
 5716            kind(expressions=[] if with_prefix else self._parse_wrapped_csv(self._parse_bitwise))
 5717        )
 5718
 5719    def _parse_grouping_sets(self) -> exp.GroupingSets | None:
 5720        if self._match(TokenType.GROUPING_SETS):
 5721            return self.expression(
 5722                exp.GroupingSets(expressions=self._parse_wrapped_csv(self._parse_grouping_set))
 5723            )
 5724        return None
 5725
 5726    def _parse_grouping_set(self) -> exp.Expr | None:
 5727        return self._parse_grouping_sets() or self._parse_cube_or_rollup() or self._parse_bitwise()
 5728
 5729    def _parse_having(self, skip_having_token: bool = False) -> exp.Having | None:
 5730        if not skip_having_token and not self._match(TokenType.HAVING):
 5731            return None
 5732        comments = self._prev_comments
 5733        return self.expression(
 5734            exp.Having(this=self._parse_disjunction()),
 5735            comments=comments,
 5736        )
 5737
 5738    def _parse_qualify(self) -> exp.Qualify | None:
 5739        if not self._match(TokenType.QUALIFY):
 5740            return None
 5741        return self.expression(exp.Qualify(this=self._parse_disjunction()))
 5742
 5743    def _parse_connect_with_prior(self) -> exp.Expr | None:
 5744        self.NO_PAREN_FUNCTION_PARSERS["PRIOR"] = lambda self: self.expression(
 5745            exp.Prior(this=self._parse_bitwise())
 5746        )
 5747        connect = self._parse_disjunction()
 5748        self.NO_PAREN_FUNCTION_PARSERS.pop("PRIOR")
 5749        return connect
 5750
 5751    def _parse_connect(self, skip_start_token: bool = False) -> exp.Connect | None:
 5752        if skip_start_token:
 5753            start = None
 5754        elif self._match_text_seq("START", "WITH"):
 5755            start = self._parse_disjunction()
 5756        else:
 5757            return None
 5758
 5759        self._match(TokenType.CONNECT_BY)
 5760        nocycle = self._match_text_seq("NOCYCLE")
 5761        connect = self._parse_connect_with_prior()
 5762
 5763        if not start and self._match_text_seq("START", "WITH"):
 5764            start = self._parse_disjunction()
 5765
 5766        return self.expression(exp.Connect(start=start, connect=connect, nocycle=nocycle))
 5767
 5768    def _parse_name_as_expression(self) -> exp.Expr | None:
 5769        this = self._parse_id_var(any_token=True)
 5770        if self._match(TokenType.ALIAS):
 5771            this = self.expression(exp.Alias(alias=this, this=self._parse_disjunction()))
 5772        return this
 5773
 5774    def _parse_interpolate(self) -> list[exp.Expr] | None:
 5775        if self._match_text_seq("INTERPOLATE"):
 5776            return self._parse_wrapped_csv(self._parse_name_as_expression)
 5777        return None
 5778
 5779    def _parse_order(
 5780        self, this: exp.Expr | None = None, skip_order_token: bool = False
 5781    ) -> exp.Expr | None:
 5782        siblings = None
 5783        if not skip_order_token and not self._match(TokenType.ORDER_BY):
 5784            if not self._match(TokenType.ORDER_SIBLINGS_BY):
 5785                return this
 5786
 5787            siblings = True
 5788
 5789        comments = self._prev_comments
 5790        return self.expression(
 5791            exp.Order(
 5792                this=this,
 5793                expressions=self._parse_csv(self._parse_ordered),
 5794                siblings=siblings,
 5795            ),
 5796            comments=comments,
 5797        )
 5798
 5799    def _parse_sort(self, exp_class: type[E], token: TokenType) -> E | None:
 5800        if not self._match(token):
 5801            return None
 5802        return self.expression(exp_class(expressions=self._parse_csv(self._parse_ordered)))
 5803
 5804    def _parse_ordered(
 5805        self, parse_method: t.Callable[[], exp.Expr | None] | None = None
 5806    ) -> exp.Ordered | None:
 5807        this = parse_method() if parse_method else self._parse_disjunction()
 5808        if not this:
 5809            return None
 5810
 5811        if this.name.upper() == "ALL" and self.dialect.SUPPORTS_ORDER_BY_ALL:
 5812            this = exp.var("ALL")
 5813
 5814        asc = self._match(TokenType.ASC)
 5815        desc: bool | None = True if self._match(TokenType.DESC) else (False if asc else None)
 5816
 5817        is_nulls_first = self._match_text_seq("NULLS", "FIRST")
 5818        is_nulls_last = self._match_text_seq("NULLS", "LAST")
 5819
 5820        nulls_first = is_nulls_first or False
 5821        explicitly_null_ordered = is_nulls_first or is_nulls_last
 5822
 5823        if (
 5824            not explicitly_null_ordered
 5825            and (
 5826                (not desc and self.dialect.NULL_ORDERING == "nulls_are_small")
 5827                or (desc and self.dialect.NULL_ORDERING != "nulls_are_small")
 5828            )
 5829            and self.dialect.NULL_ORDERING != "nulls_are_last"
 5830        ):
 5831            nulls_first = True
 5832
 5833        if self._match_text_seq("WITH", "FILL"):
 5834            with_fill = self.expression(
 5835                exp.WithFill(
 5836                    from_=self._match(TokenType.FROM) and self._parse_bitwise(),
 5837                    to=self._match_text_seq("TO") and self._parse_bitwise(),
 5838                    step=self._match_text_seq("STEP") and self._parse_bitwise(),
 5839                    interpolate=self._parse_interpolate(),
 5840                )
 5841            )
 5842        else:
 5843            with_fill = None
 5844
 5845        return self.expression(
 5846            exp.Ordered(this=this, desc=desc, nulls_first=nulls_first, with_fill=with_fill)
 5847        )
 5848
 5849    def _parse_limit_options(self) -> exp.LimitOptions | None:
 5850        percent = self._match_set((TokenType.PERCENT, TokenType.MOD))
 5851        rows = self._match_texts(("ROW", "ROWS"))
 5852        self._match_text_seq("ONLY")
 5853        with_ties = self._match_text_seq("WITH", "TIES")
 5854
 5855        if not (percent or rows or with_ties):
 5856            return None
 5857
 5858        return self.expression(exp.LimitOptions(percent=percent, rows=rows, with_ties=with_ties))
 5859
 5860    def _parse_limit(
 5861        self,
 5862        this: exp.Expr | None = None,
 5863        top: bool = False,
 5864        skip_limit_token: bool = False,
 5865    ) -> exp.Expr | None:
 5866        if skip_limit_token or self._match(TokenType.TOP if top else TokenType.LIMIT):
 5867            comments = self._prev_comments
 5868            if top:
 5869                limit_paren = self._match(TokenType.L_PAREN)
 5870                expression = (
 5871                    self._parse_term() or self._parse_select()
 5872                    if limit_paren
 5873                    else self._parse_number()
 5874                )
 5875
 5876                if limit_paren:
 5877                    self._match_r_paren()
 5878
 5879            else:
 5880                if self.dialect.SUPPORTS_LIMIT_ALL and self._match(TokenType.ALL):
 5881                    expression = exp.var("ALL")
 5882                else:
 5883                    expression = self._parse_term(parse_mod=False)
 5884            limit_options = self._parse_limit_options()
 5885
 5886            if self._match(TokenType.COMMA):
 5887                offset = expression
 5888                expression = self._parse_term()
 5889            else:
 5890                offset = None
 5891
 5892            limit_exp = self.expression(
 5893                exp.Limit(
 5894                    this=this,
 5895                    expression=expression,
 5896                    offset=offset,
 5897                    limit_options=limit_options,
 5898                    expressions=self._parse_limit_by(),
 5899                ),
 5900                comments=comments,
 5901            )
 5902
 5903            if top:
 5904                limit_exp.meta["top"] = True
 5905
 5906            return limit_exp
 5907
 5908        if self._match(TokenType.FETCH):
 5909            direction = (
 5910                self._prev.text.upper()
 5911                if self._match_set((TokenType.FIRST, TokenType.NEXT))
 5912                else "FIRST"
 5913            )
 5914
 5915            count = (
 5916                None
 5917                if self._match_texts(("ROW", "ROWS"), advance=False)
 5918                else self._parse_field(tokens=self.FETCH_TOKENS)
 5919            )
 5920
 5921            return self.expression(
 5922                exp.Fetch(
 5923                    direction=direction, count=count, limit_options=self._parse_limit_options()
 5924                )
 5925            )
 5926
 5927        return this
 5928
 5929    def _parse_offset(self, this: exp.Expr | None = None) -> exp.Expr | None:
 5930        if not self._match(TokenType.OFFSET):
 5931            return this
 5932
 5933        count = self._parse_term()
 5934        self._match_texts(("ROW", "ROWS"))
 5935
 5936        return self.expression(
 5937            exp.Offset(this=this, expression=count, expressions=self._parse_limit_by())
 5938        )
 5939
 5940    def _can_parse_limit_or_offset(self) -> bool:
 5941        if not self._match_set(self.AMBIGUOUS_ALIAS_TOKENS, advance=False):
 5942            return False
 5943
 5944        index = self._index
 5945        result = bool(
 5946            self._try_parse(self._parse_limit, retreat=True)
 5947            or self._try_parse(self._parse_offset, retreat=True)
 5948        )
 5949        self._retreat(index)
 5950
 5951        # MATCH_CONDITION (...) is a special construct that should not be consumed by limit/offset
 5952        if self._next.token_type == TokenType.MATCH_CONDITION:
 5953            result = False
 5954
 5955        return result
 5956
 5957    def _can_parse_named_window(self) -> bool:
 5958        # `WINDOW` is in ID_VAR_TOKENS so it could be mistakenly consumed as an implicit alias.
 5959        # Refuse only when the following tokens look like a named-window clause: `WINDOW <id> AS (`.
 5960        if not self._match(TokenType.WINDOW, advance=False):
 5961            return False
 5962
 5963        name = self._tokens[self._index + 1] if self._index + 1 < len(self._tokens) else None
 5964        if name is None or name.token_type not in self.ID_VAR_TOKENS:
 5965            return False
 5966
 5967        alias_tok = self._tokens[self._index + 2] if self._index + 2 < len(self._tokens) else None
 5968        if alias_tok is None or alias_tok.token_type != TokenType.ALIAS:
 5969            return False
 5970
 5971        body = self._tokens[self._index + 3] if self._index + 3 < len(self._tokens) else None
 5972        return body is not None and body.token_type == TokenType.L_PAREN
 5973
 5974    def _parse_limit_by(self) -> list[exp.Expr] | None:
 5975        return self._parse_csv(self._parse_bitwise) if self._match_text_seq("BY") else None
 5976
 5977    def _parse_locks(self) -> list[exp.Lock]:
 5978        locks = []
 5979        while True:
 5980            update, key = None, None
 5981            if self._match_text_seq("FOR", "UPDATE"):
 5982                update = True
 5983            elif self._match_text_seq("FOR", "SHARE") or self._match_text_seq(
 5984                "LOCK", "IN", "SHARE", "MODE"
 5985            ):
 5986                update = False
 5987            elif self._match_text_seq("FOR", "KEY", "SHARE"):
 5988                update, key = False, True
 5989            elif self._match_text_seq("FOR", "NO", "KEY", "UPDATE"):
 5990                update, key = True, True
 5991            else:
 5992                break
 5993
 5994            expressions = None
 5995            if self._match_text_seq("OF"):
 5996                expressions = self._parse_csv(lambda: self._parse_table(schema=True))
 5997
 5998            wait: bool | exp.Expr | None = None
 5999            if self._match_text_seq("NOWAIT"):
 6000                wait = True
 6001            elif self._match_text_seq("WAIT"):
 6002                wait = self._parse_primary()
 6003            elif self._match_text_seq("SKIP", "LOCKED"):
 6004                wait = False
 6005
 6006            locks.append(
 6007                self.expression(
 6008                    exp.Lock(update=update, expressions=expressions, wait=wait, key=key)
 6009                )
 6010            )
 6011
 6012        return locks
 6013
 6014    def parse_set_operation(
 6015        self, this: exp.Expr | None, consume_pipe: bool = False
 6016    ) -> exp.Expr | None:
 6017        start = self._index
 6018        _, side_token, kind_token = self._parse_join_parts()
 6019
 6020        side = side_token.text if side_token else None
 6021        kind = kind_token.text if kind_token else None
 6022
 6023        if not self._match_set(self.SET_OPERATIONS):
 6024            self._retreat(start)
 6025            return None
 6026
 6027        token_type = self._prev.token_type
 6028
 6029        if token_type == TokenType.UNION:
 6030            operation: type[exp.SetOperation] = exp.Union
 6031        elif token_type == TokenType.EXCEPT:
 6032            operation = exp.Except
 6033        else:
 6034            operation = exp.Intersect
 6035
 6036        comments = self._prev.comments
 6037
 6038        if self._match(TokenType.DISTINCT):
 6039            distinct: bool | None = True
 6040        elif self._match(TokenType.ALL):
 6041            distinct = False
 6042        else:
 6043            distinct = self.dialect.SET_OP_DISTINCT_BY_DEFAULT[operation]
 6044            if distinct is None:
 6045                self.raise_error(f"Expected DISTINCT or ALL for {operation.__name__}")
 6046
 6047        by_name = (
 6048            self._match_text_seq("BY", "NAME")
 6049            or self._match_text_seq("STRICT", "CORRESPONDING")
 6050            or None
 6051        )
 6052        if self._match_text_seq("CORRESPONDING"):
 6053            by_name = True
 6054            if not side and not kind:
 6055                kind = "INNER"
 6056
 6057        on_column_list = None
 6058        if by_name and self._match_texts(("ON", "BY")):
 6059            on_column_list = self._parse_wrapped_csv(self._parse_column)
 6060
 6061        expression = self._parse_select(
 6062            nested=True, parse_set_operation=False, consume_pipe=consume_pipe
 6063        )
 6064
 6065        # Wrap VALUES operands in selects, both for consistency with the CTE canonicalization
 6066        # in _parse_cte and so that alias pushdown can reach into set operation branches
 6067        if isinstance(this, exp.Values):
 6068            this = self._values_to_select(this)
 6069        if isinstance(expression, exp.Values):
 6070            expression = self._values_to_select(expression)
 6071
 6072        if isinstance(this, exp.Alias) and isinstance(this.this, exp.Subquery):
 6073            subquery = this.this
 6074            subquery.set("alias", exp.TableAlias(this=this.args["alias"]))
 6075            subquery.add_comments(this.pop_comments())
 6076            this = subquery
 6077
 6078        return self.expression(
 6079            operation(
 6080                this=this,
 6081                distinct=distinct,
 6082                by_name=by_name,
 6083                expression=expression,
 6084                side=side,
 6085                kind=kind,
 6086                on=on_column_list,
 6087            ),
 6088            comments=comments,
 6089        )
 6090
 6091    def _parse_set_operations(self, this: exp.Expr | None) -> exp.Expr | None:
 6092        while this:
 6093            setop = self.parse_set_operation(this)
 6094            if not setop:
 6095                break
 6096            this = setop
 6097
 6098        if isinstance(this, exp.SetOperation) and self.MODIFIERS_ATTACHED_TO_SET_OP:
 6099            expression = this.expression
 6100
 6101            if expression:
 6102                for arg in self.SET_OP_MODIFIERS:
 6103                    expr = expression.args.get(arg)
 6104                    if expr and not (arg == "limit" and expr.meta.get("top")):
 6105                        expression.set(arg, None)
 6106                        this.set(arg, expr)
 6107
 6108            # A trailing LIMIT/FETCH can coexist with TOP on the final operand.
 6109            if self._curr.token_type in (TokenType.LIMIT, TokenType.FETCH):
 6110                this = self._parse_query_modifiers(this)
 6111
 6112        return this
 6113
 6114    def _parse_expression(self) -> exp.Expr | None:
 6115        return self._parse_alias(self._parse_assignment())
 6116
 6117    def _parse_assignment(self) -> exp.Expr | None:
 6118        this = self._parse_disjunction()
 6119        if not this and self._next.token_type in self.ASSIGNMENT:
 6120            # This allows us to parse <non-identifier token> := <expr>
 6121            this = exp.column(
 6122                t.cast(str, self._advance_any(ignore_reserved=True) and self._prev.text)
 6123            )
 6124
 6125        while self._match_set(self.ASSIGNMENT):
 6126            if isinstance(this, exp.Column) and len(this.parts) == 1:
 6127                this = this.this
 6128
 6129            comments = self._prev_comments
 6130            this = self.expression(
 6131                self.ASSIGNMENT[self._prev.token_type](
 6132                    this=this, expression=self._parse_assignment()
 6133                ),
 6134                comments=comments,
 6135            )
 6136
 6137        return this
 6138
 6139    def _parse_disjunction(self) -> exp.Expr | None:
 6140        this = self._parse_conjunction()
 6141        while self._match_set(self.DISJUNCTION):
 6142            comments = self._prev_comments
 6143            this = self.expression(
 6144                self.DISJUNCTION[self._prev.token_type](
 6145                    this=this, expression=self._parse_conjunction()
 6146                ),
 6147                comments=comments,
 6148            )
 6149        return this
 6150
 6151    def _parse_conjunction(self) -> exp.Expr | None:
 6152        this = self._parse_equality()
 6153        while self._match_set(self.CONJUNCTION):
 6154            comments = self._prev_comments
 6155            this = self.expression(
 6156                self.CONJUNCTION[self._prev.token_type](
 6157                    this=this, expression=self._parse_equality()
 6158                ),
 6159                comments=comments,
 6160            )
 6161        return this
 6162
 6163    def _parse_equality(self) -> exp.Expr | None:
 6164        this = self._parse_comparison()
 6165        while self._match_set(self.EQUALITY):
 6166            comments = self._prev_comments
 6167            this = self.expression(
 6168                self.EQUALITY[self._prev.token_type](
 6169                    this=this, expression=self._parse_comparison()
 6170                ),
 6171                comments=comments,
 6172            )
 6173        return this
 6174
 6175    def _parse_comparison(self) -> exp.Expr | None:
 6176        this = self._parse_range()
 6177        while self._match_set(self.COMPARISON):
 6178            comments = self._prev_comments
 6179            this = self.expression(
 6180                self.COMPARISON[self._prev.token_type](this=this, expression=self._parse_range()),
 6181                comments=comments,
 6182            )
 6183        return this
 6184
 6185    def _parse_range(self, this: exp.Expr | None = None) -> exp.Expr | None:
 6186        this = this or self._parse_bitwise()
 6187
 6188        while True:
 6189            negate = self._match(TokenType.NOT)
 6190            if self._match_set(self.RANGE_PARSERS):
 6191                expression = self.RANGE_PARSERS[self._prev.token_type](self, this)
 6192                if not expression:
 6193                    return this
 6194
 6195                this = expression
 6196            elif self._match(TokenType.ISNULL) or (negate and self._match(TokenType.NULL)):
 6197                this = self.expression(exp.Is(this=this, expression=exp.Null()))
 6198            elif self._match(TokenType.NOTNULL):
 6199                # Postgres supports ISNULL and NOTNULL for conditions.
 6200                # https://blog.andreiavram.ro/postgresql-null-composite-type/
 6201                if self.dialect.NORMALIZE_NOT_NULL:
 6202                    this = self.expression(exp.Is(this=this, expression=exp.Null()))
 6203                    this = self.expression(exp.Not(this=this))
 6204                else:
 6205                    this = self.expression(exp.Is(this=this, expression=exp.Null(), negate=True))
 6206            else:
 6207                if negate:
 6208                    self._retreat(self._index - 1)
 6209                break
 6210
 6211            if negate:
 6212                this = self._negate_range(this)
 6213                if self._curr and (
 6214                    self._curr.token_type == TokenType.NOT
 6215                    or self._curr.token_type in self.RANGE_PARSERS
 6216                ):
 6217                    this = self.expression(exp.Paren(this=this))
 6218
 6219        return this
 6220
 6221    def _negate_range(self, this: exp.Expr | None = None) -> exp.Expr | None:
 6222        if not this:
 6223            return this
 6224
 6225        expression = this.this if isinstance(this, exp.Escape) else this
 6226        if isinstance(expression, (exp.Like, exp.ILike)):
 6227            expression.set("negate", True)
 6228            return this
 6229
 6230        return self.expression(exp.Not(this=this))
 6231
 6232    def _parse_is(self, this: exp.Expr | None) -> exp.Expr | None:
 6233        index = self._index - 1
 6234        negate = self._match(TokenType.NOT)
 6235
 6236        if self._match_text_seq("DISTINCT", "FROM"):
 6237            klass = exp.NullSafeEQ if negate else exp.NullSafeNEQ
 6238            return self.expression(klass(this=this, expression=self._parse_bitwise()))
 6239
 6240        if self._match(TokenType.JSON):
 6241            kind = self._match_texts(self.IS_JSON_PREDICATE_KIND) and self._prev.text.upper()
 6242
 6243            if self._match_text_seq("WITH"):
 6244                _with = True
 6245            elif self._match_text_seq("WITHOUT"):
 6246                _with = False
 6247            else:
 6248                _with = None
 6249
 6250            unique = self._match(TokenType.UNIQUE)
 6251            self._match_text_seq("KEYS")
 6252            expression: exp.Expr | None = self.expression(
 6253                exp.JSON(this=kind, with_=_with, unique=unique)
 6254            )
 6255        else:
 6256            expression = self._parse_null() or self._parse_bitwise()
 6257            if not expression:
 6258                self._retreat(index)
 6259                return None
 6260
 6261        if negate and isinstance(expression, exp.Null) and not self.dialect.NORMALIZE_NOT_NULL:
 6262            this = self.expression(exp.Is(this=this, expression=expression, negate=True))
 6263        else:
 6264            this = self.expression(exp.Is(this=this, expression=expression))
 6265            this = self.expression(exp.Not(this=this)) if negate else this
 6266
 6267        return self._parse_column_ops(this)
 6268
 6269    def _parse_in(self, this: exp.Expr | None, alias: bool = False) -> exp.In:
 6270        unnest = self._parse_unnest(with_alias=False)
 6271        if unnest:
 6272            this = self.expression(exp.In(this=this, unnest=unnest))
 6273        elif self._match_set((TokenType.L_PAREN, TokenType.L_BRACKET)):
 6274            matched_l_paren = self._prev.token_type == TokenType.L_PAREN
 6275            expressions = self._parse_csv(lambda: self._parse_select_or_expression(alias=alias))
 6276
 6277            if len(expressions) == 1 and isinstance(query := expressions[0], exp.Query):
 6278                this = self.expression(
 6279                    exp.In(this=this, query=self._parse_query_modifiers(query).subquery(copy=False))
 6280                )
 6281            else:
 6282                this = self.expression(exp.In(this=this, expressions=expressions))
 6283
 6284            if matched_l_paren:
 6285                self._match_r_paren(this)
 6286            elif not self._match(TokenType.R_BRACKET, expression=this):
 6287                self.raise_error("Expecting ]")
 6288        else:
 6289            this = self.expression(exp.In(this=this, field=self._parse_column()))
 6290
 6291        return this
 6292
 6293    def _parse_between(self, this: exp.Expr | None) -> exp.Between:
 6294        symmetric = None
 6295        if self._match_text_seq("SYMMETRIC"):
 6296            symmetric = True
 6297        elif self._match_text_seq("ASYMMETRIC"):
 6298            symmetric = False
 6299
 6300        low = self._parse_bitwise()
 6301        self._match(TokenType.AND)
 6302        high = self._parse_bitwise()
 6303
 6304        return self.expression(exp.Between(this=this, low=low, high=high, symmetric=symmetric))
 6305
 6306    def _parse_escape(self, this: exp.Expr | None) -> exp.Expr | None:
 6307        if not self._match(TokenType.ESCAPE):
 6308            return this
 6309        return self.expression(
 6310            exp.Escape(this=this, expression=self._parse_string() or self._parse_null())
 6311        )
 6312
 6313    def _parse_interval_span(
 6314        self, this: exp.Expr, parse_function_unit: bool = True
 6315    ) -> exp.Interval:
 6316        # handle day-time format interval span with omitted units:
 6317        #   INTERVAL '<number days> hh[:][mm[:ss[.ff]]]' <maybe `unit TO unit`>
 6318        interval_span_units_omitted = None
 6319        if (
 6320            this
 6321            and this.is_string
 6322            and self.SUPPORTS_OMITTED_INTERVAL_SPAN_UNIT
 6323            and exp.INTERVAL_DAY_TIME_RE.match(this.name)
 6324        ):
 6325            index = self._index
 6326
 6327            # Var "TO" Var
 6328            first_unit = self._parse_var(any_token=True, upper=True)
 6329            second_unit = None
 6330            if first_unit and self._match_text_seq("TO"):
 6331                second_unit = self._parse_var(any_token=True, upper=True)
 6332
 6333            interval_span_units_omitted = not (first_unit and second_unit)
 6334
 6335            self._retreat(index)
 6336
 6337        unit_index = self._index
 6338        if interval_span_units_omitted:
 6339            unit = None
 6340        else:
 6341            # Only attempt to parse a unit if the current token can actually be one, so that a
 6342            # trailing operator isn't swallowed, e.g. INTERVAL '1 day' AND (x)
 6343            is_unit = self._curr is not None and (
 6344                self._curr.token_type == TokenType.VAR
 6345                or self._curr.text.upper() in self.dialect.VALID_INTERVAL_UNITS
 6346            )
 6347            unit = self._parse_function() if parse_function_unit and is_unit else None
 6348            if not unit and is_unit:
 6349                unit = self._parse_var(any_token=True, upper=True)
 6350
 6351        # Most dialects support, e.g., the form INTERVAL '5' day, thus we try to parse
 6352        # each INTERVAL expression into this canonical form so it's easy to transpile
 6353        if this and this.is_number:
 6354            try:
 6355                this = exp.Literal.string(this.to_py())
 6356            except ValueError:
 6357                self.raise_error(f"Invalid numeric interval literal: {this.name!r}")
 6358        elif this and this.is_string:
 6359            parts = exp.INTERVAL_STRING_RE.findall(this.name)
 6360            if parts and unit:
 6361                # Unconsume the eagerly-parsed unit, since the real unit was part of the string
 6362                unit = None
 6363                self._retreat(unit_index)
 6364
 6365            if len(parts) == 1:
 6366                this = exp.Literal.string(parts[0][0])
 6367                unit = self.expression(exp.Var(this=parts[0][1].upper()))
 6368
 6369        if self.INTERVAL_SPANS and self._match_text_seq("TO"):
 6370            unit = self.expression(
 6371                exp.IntervalSpan(
 6372                    this=unit,
 6373                    expression=self._parse_function()
 6374                    or self._parse_var(any_token=True, upper=True),
 6375                )
 6376            )
 6377
 6378        return self.expression(exp.Interval(this=this, unit=unit))
 6379
 6380    def _parse_interval(
 6381        self, require_interval: bool = True, parse_function_unit: bool = True
 6382    ) -> exp.Add | exp.Interval | None:
 6383        index = self._index
 6384
 6385        if not self._match(TokenType.INTERVAL) and require_interval:
 6386            return None
 6387
 6388        if self._match(TokenType.STRING, advance=False):
 6389            this = self._parse_primary()
 6390        else:
 6391            this = self._parse_term()
 6392
 6393        if not this or (
 6394            isinstance(this, exp.Column)
 6395            and not this.table
 6396            and not this.this.quoted
 6397            and self._curr
 6398            and self._curr.text.upper() not in self.dialect.VALID_INTERVAL_UNITS
 6399        ):
 6400            self._retreat(index)
 6401            return None
 6402
 6403        interval = self._parse_interval_span(this, parse_function_unit=parse_function_unit)
 6404
 6405        index = self._index
 6406        self._match(TokenType.PLUS)
 6407
 6408        # Convert INTERVAL 'val_1' unit_1 [+] ... [+] 'val_n' unit_n into a sum of intervals
 6409        if self._match_set((TokenType.STRING, TokenType.NUMBER), advance=False):
 6410            return self.expression(
 6411                exp.Add(
 6412                    this=interval,
 6413                    expression=self._parse_interval(False, parse_function_unit=parse_function_unit),
 6414                )
 6415            )
 6416
 6417        self._retreat(index)
 6418        return interval
 6419
 6420    def _parse_bitwise(self) -> exp.Expr | None:
 6421        this = self._parse_term()
 6422
 6423        while True:
 6424            if self._match_set(self.BITWISE):
 6425                this = self.expression(
 6426                    self.BITWISE[self._prev.token_type](this=this, expression=self._parse_term())
 6427                )
 6428            elif self.dialect.DPIPE_IS_STRING_CONCAT and self._match(TokenType.DPIPE):
 6429                this = self.expression(
 6430                    exp.DPipe(
 6431                        this=this,
 6432                        expression=self._parse_term(),
 6433                        safe=not self.dialect.STRICT_STRING_CONCAT,
 6434                    )
 6435                )
 6436            elif self._match(TokenType.DQMARK):
 6437                this = self.expression(
 6438                    exp.Coalesce(this=this, expressions=ensure_list(self._parse_term()))
 6439                )
 6440            elif self._match_pair(TokenType.LT, TokenType.LT):
 6441                this = self.expression(
 6442                    exp.BitwiseLeftShift(this=this, expression=self._parse_term())
 6443                )
 6444            elif self._match_pair(TokenType.GT, TokenType.GT):
 6445                this = self.expression(
 6446                    exp.BitwiseRightShift(this=this, expression=self._parse_term())
 6447                )
 6448            elif self.JSON_OPERATORS and self._match_set(self.JSON_OPERATORS):
 6449                this = self.JSON_OPERATORS[self._prev.token_type](self, this, self._parse_term())
 6450            else:
 6451                break
 6452
 6453        return this
 6454
 6455    def _parse_term(self, parse_mod: bool = True) -> exp.Expr | None:
 6456        this = self._parse_factor(parse_mod=parse_mod)
 6457
 6458        while self._match_set(self.TERM):
 6459            klass = self.TERM[self._prev.token_type]
 6460            comments = self._prev_comments
 6461            expression = self._parse_factor(parse_mod=parse_mod)
 6462
 6463            this = self.expression(klass(this=this, expression=expression), comments=comments)
 6464
 6465            if isinstance(this, exp.Collate):
 6466                self._normalize_collate(this)
 6467
 6468        return this
 6469
 6470    def _normalize_collate(self, collate: exp.Collate) -> None:
 6471        expr = collate.expression
 6472
 6473        # Preserve collations such as pg_catalog."default" (Postgres) as columns, otherwise
 6474        # fallback to Identifier / Var
 6475        if isinstance(expr, exp.Column) and len(expr.parts) == 1:
 6476            ident = expr.this
 6477            if isinstance(ident, exp.Identifier):
 6478                collate.set("expression", ident if ident.quoted else exp.var(ident.name))
 6479
 6480    def _parse_factor(self, parse_mod: bool = True) -> exp.Expr | None:
 6481        parse_method = self._parse_factor_operand
 6482        this = self._parse_at_time_zone(parse_method())
 6483
 6484        while self._match_set(self.FACTOR, advance=False):
 6485            if not parse_mod and self._curr.token_type == TokenType.MOD:
 6486                break
 6487
 6488            self._advance()
 6489            klass = self.FACTOR[self._prev.token_type]
 6490            comments = self._prev_comments
 6491            expression = parse_method()
 6492
 6493            if not expression and klass is exp.IntDiv and self._prev.text.isalpha():
 6494                self._retreat(self._index - 1)
 6495                return this
 6496
 6497            this = self.expression(klass(this=this, expression=expression), comments=comments)
 6498
 6499            if isinstance(this, exp.Div):
 6500                this.set("typed", self.dialect.TYPED_DIVISION)
 6501                this.set("safe", self.dialect.SAFE_DIVISION)
 6502
 6503        return this
 6504
 6505    def _parse_factor_operand(self) -> exp.Expr | None:
 6506        return self._parse_exponent() if self.EXPONENT else self._parse_unary()
 6507
 6508    def _parse_exponent(self) -> exp.Expr | None:
 6509        this = self._parse_unary()
 6510        while self._match_set(self.EXPONENT):
 6511            comments = self._prev_comments
 6512            this = self.expression(
 6513                self.EXPONENT[self._prev.token_type](this=this, expression=self._parse_unary()),
 6514                comments=comments,
 6515            )
 6516        return this
 6517
 6518    def _parse_unary(self) -> exp.Expr | None:
 6519        if self._match_set(self.UNARY_PARSERS):
 6520            return self.UNARY_PARSERS[self._prev.token_type](self)
 6521        return self._parse_type()
 6522
 6523    def _parse_type(
 6524        self, parse_interval: bool = True, fallback_to_identifier: bool = False
 6525    ) -> exp.Expr | None:
 6526        if not fallback_to_identifier and (atom := self._parse_atom()) is not None:
 6527            return atom
 6528
 6529        if interval := parse_interval and self._parse_interval():
 6530            return self._parse_column_ops(interval)
 6531
 6532        index = self._index
 6533        data_type = self._parse_types(check_func=True, allow_identifiers=False)
 6534
 6535        # parse_types() returns a Cast if we parsed BQ's inline constructor <type>(<values>) e.g.
 6536        # STRUCT<a INT, b STRING>(1, 'foo'), which is canonicalized to CAST(<values> AS <type>)
 6537        if isinstance(data_type, exp.Cast):
 6538            # This constructor can contain ops directly after it, for instance struct unnesting:
 6539            # STRUCT<a INT, b STRING>(1, 'foo').* --> CAST(STRUCT(1, 'foo') AS STRUCT<a iNT, b STRING).*
 6540            return self._parse_column_ops(data_type)
 6541
 6542        if data_type:
 6543            index2 = self._index
 6544            this = self._parse_primary()
 6545
 6546            if isinstance(this, exp.Literal):
 6547                literal = this.name
 6548                this = self._parse_column_ops(this)
 6549
 6550                if self.ZONE_AWARE_TIMESTAMP_CONSTRUCTOR and TIME_ZONE_RE.search(literal):
 6551                    if data_type.is_type(exp.DType.TIMESTAMP):
 6552                        data_type = exp.DType.TIMESTAMPTZ.into_expr()
 6553                    elif data_type.is_type(exp.DType.TIME):
 6554                        data_type = exp.DType.TIMETZ.into_expr()
 6555
 6556                parser = self.TYPE_LITERAL_PARSERS.get(data_type.this)
 6557                if parser:
 6558                    return parser(self, this, data_type)
 6559
 6560                return self.expression(exp.Cast(this=this, to=data_type))
 6561
 6562            # The expressions arg gets set by the parser when we have something like DECIMAL(38, 0)
 6563            # in the input SQL. In that case, we'll produce these tokens: DECIMAL ( 38 , 0 )
 6564            #
 6565            # If the index difference here is greater than 1, that means the parser itself must have
 6566            # consumed additional tokens such as the DECIMAL scale and precision in the above example.
 6567            #
 6568            # If it's not greater than 1, then it must be 1, because we've consumed at least the type
 6569            # keyword, meaning that the expressions arg of the DataType must have gotten set by a
 6570            # callable in the TYPE_CONVERTERS mapping. For example, Snowflake converts DECIMAL to
 6571            # DECIMAL(38, 0)) in order to facilitate the data type's transpilation.
 6572            #
 6573            # In these cases, we don't really want to return the converted type, but instead retreat
 6574            # and try to parse a Column or Identifier in the section below.
 6575            if data_type.expressions and index2 - index > 1:
 6576                self._retreat(index2)
 6577                return self._parse_column_ops(data_type)
 6578
 6579            self._retreat(index)
 6580
 6581        if fallback_to_identifier:
 6582            return self._parse_id_var()
 6583
 6584        return self._parse_column()
 6585
 6586    def _parse_type_size(self) -> exp.DataTypeParam | None:
 6587        this = self._parse_type()
 6588        if not this:
 6589            return None
 6590
 6591        if isinstance(this, exp.Column) and not this.table:
 6592            this = exp.var(this.name.upper())
 6593
 6594        return self.expression(
 6595            exp.DataTypeParam(this=this, expression=self._parse_var(any_token=True))
 6596        )
 6597
 6598    def _parse_user_defined_type(self, identifier: exp.Identifier) -> exp.Expr | None:
 6599        type_name = identifier.name
 6600
 6601        while self._match(TokenType.DOT):
 6602            type_name = f"{type_name}.{self._advance_any() and self._prev.text}"
 6603
 6604        return exp.DataType.from_str(type_name, dialect=self.dialect, udt=True)
 6605
 6606    def _parse_types(
 6607        self,
 6608        check_func: bool = False,
 6609        schema: bool = False,
 6610        allow_identifiers: bool = True,
 6611        with_collation: bool = False,
 6612    ) -> exp.Expr | None:
 6613        index = self._index
 6614        this: exp.Expr | None = None
 6615
 6616        if self._match_set(self.TYPE_TOKENS):
 6617            type_token = self._prev.token_type
 6618        else:
 6619            type_token = None
 6620            identifier = allow_identifiers and self._parse_id_var(
 6621                any_token=False, tokens=(TokenType.VAR,)
 6622            )
 6623            if isinstance(identifier, exp.Identifier):
 6624                if identifier.quoted and identifier.name in self.QUOTED_TYPES_TO_PRESERVE:
 6625                    this = exp.DataType.build(identifier, udt=True)
 6626                else:
 6627                    try:
 6628                        tokens = self.dialect.tokenize(identifier.name)
 6629                    except TokenError:
 6630                        tokens = None
 6631
 6632                    if tokens and (type_token := tokens[0].token_type) in self.TYPE_TOKENS:
 6633                        if len(tokens) > 1:
 6634                            return exp.DataType.from_str(identifier.name, dialect=self.dialect)
 6635                    elif self.dialect.SUPPORTS_USER_DEFINED_TYPES:
 6636                        this = self._parse_user_defined_type(identifier)
 6637                    else:
 6638                        self._retreat(self._index - 1)
 6639                        return None
 6640            else:
 6641                return None
 6642
 6643        if type_token == TokenType.PSEUDO_TYPE:
 6644            return self.expression(exp.PseudoType(this=self._prev.text.upper()))
 6645
 6646        if type_token == TokenType.OBJECT_IDENTIFIER:
 6647            return self.expression(exp.ObjectIdentifier(this=self._prev.text.upper()))
 6648
 6649        # https://materialize.com/docs/sql/types/map/
 6650        if type_token == TokenType.MAP and self._match(TokenType.L_BRACKET):
 6651            key_type = self._parse_types(
 6652                check_func=check_func, schema=schema, allow_identifiers=allow_identifiers
 6653            )
 6654            if not self._match(TokenType.FARROW):
 6655                self._retreat(index)
 6656                return None
 6657
 6658            value_type = self._parse_types(
 6659                check_func=check_func, schema=schema, allow_identifiers=allow_identifiers
 6660            )
 6661            if not self._match(TokenType.R_BRACKET):
 6662                self._retreat(index)
 6663                return None
 6664
 6665            return exp.DataType(
 6666                this=exp.DType.MAP,
 6667                expressions=[key_type, value_type],
 6668                nested=True,
 6669            )
 6670
 6671        nested = type_token in self.NESTED_TYPE_TOKENS
 6672        is_struct = type_token in self.STRUCT_TYPE_TOKENS
 6673        is_aggregate = type_token in self.AGGREGATE_TYPE_TOKENS
 6674        expressions = None
 6675        maybe_func = False
 6676
 6677        if self._match(TokenType.L_PAREN):
 6678            if is_struct:
 6679                expressions = self._parse_csv(lambda: self._parse_struct_types(type_required=True))
 6680            elif nested:
 6681                expressions = self._parse_csv(
 6682                    lambda: self._parse_types(
 6683                        check_func=check_func, schema=schema, allow_identifiers=allow_identifiers
 6684                    )
 6685                )
 6686                if type_token == TokenType.NULLABLE and len(expressions) == 1:
 6687                    this = expressions[0]
 6688                    this.set("nullable", True)
 6689                    self._match_r_paren()
 6690                    return this
 6691            elif type_token in self.ENUM_TYPE_TOKENS:
 6692                expressions = self._parse_csv(self._parse_equality)
 6693            elif type_token == TokenType.JSON:
 6694                # ClickHouse JSON type supports arguments: JSON(col Type, SKIP col, param=value)
 6695                # https://clickhouse.com/docs/sql-reference/data-types/newjson
 6696                expressions = self._parse_csv(self._parse_json_type_arg)
 6697            elif is_aggregate:
 6698                func_or_ident = self._parse_function(anonymous=True) or self._parse_id_var(
 6699                    any_token=False, tokens=(TokenType.VAR, TokenType.ANY)
 6700                )
 6701                if not func_or_ident:
 6702                    return None
 6703                expressions = [func_or_ident]
 6704                if self._match(TokenType.COMMA):
 6705                    expressions.extend(
 6706                        self._parse_csv(
 6707                            lambda: self._parse_types(
 6708                                check_func=check_func,
 6709                                schema=schema,
 6710                                allow_identifiers=allow_identifiers,
 6711                            )
 6712                        )
 6713                    )
 6714            else:
 6715                expressions = self._parse_csv(self._parse_type_size)
 6716
 6717                # https://docs.snowflake.com/en/sql-reference/data-types-vector
 6718                if type_token == TokenType.VECTOR and len(expressions) == 2:
 6719                    expressions = self._parse_vector_expressions(expressions)
 6720
 6721            if not self._match(TokenType.R_PAREN):
 6722                self._retreat(index)
 6723                return None
 6724
 6725            maybe_func = True
 6726
 6727        values: list[exp.Expr] | None = None
 6728
 6729        if nested and self._match(TokenType.LT):
 6730            if is_struct:
 6731                expressions = self._parse_csv(lambda: self._parse_struct_types(type_required=True))
 6732            else:
 6733                expressions = self._parse_csv(
 6734                    lambda: self._parse_types(
 6735                        check_func=check_func,
 6736                        schema=schema,
 6737                        allow_identifiers=allow_identifiers,
 6738                        with_collation=True,
 6739                    )
 6740                )
 6741
 6742            if not self._match(TokenType.GT):
 6743                self.raise_error("Expecting >")
 6744
 6745            if self._match_set((TokenType.L_BRACKET, TokenType.L_PAREN)):
 6746                values = self._parse_csv(self._parse_disjunction)
 6747                if not values and is_struct:
 6748                    values = None
 6749                    self._retreat(self._index - 1)
 6750                else:
 6751                    self._match_set((TokenType.R_BRACKET, TokenType.R_PAREN))
 6752
 6753        if type_token in self.TIMESTAMPS:
 6754            if self._match_text_seq("WITH", "TIME", "ZONE"):
 6755                maybe_func = False
 6756                tz_type = exp.DType.TIMETZ if type_token in self.TIMES else exp.DType.TIMESTAMPTZ
 6757                this = exp.DataType(this=tz_type, expressions=expressions)
 6758            elif self._match_text_seq("WITH", "LOCAL", "TIME", "ZONE"):
 6759                maybe_func = False
 6760                this = exp.DataType(this=exp.DType.TIMESTAMPLTZ, expressions=expressions)
 6761            elif self._match_text_seq("WITHOUT", "TIME", "ZONE"):
 6762                maybe_func = False
 6763        elif type_token == TokenType.INTERVAL:
 6764            if self._curr.text.upper() in self.dialect.VALID_INTERVAL_UNITS:
 6765                unit = self._parse_var(upper=True)
 6766                if self._match_text_seq("TO"):
 6767                    unit = exp.IntervalSpan(this=unit, expression=self._parse_var(upper=True))
 6768
 6769                this = self.expression(exp.DataType(this=self.expression(exp.Interval(unit=unit))))
 6770            else:
 6771                this = self.expression(exp.DataType(this=exp.DType.INTERVAL))
 6772        elif type_token == TokenType.VOID:
 6773            this = exp.DataType(this=exp.DType.NULL)
 6774
 6775        if maybe_func and check_func:
 6776            index2 = self._index
 6777            peek = self._parse_string()
 6778
 6779            if not peek:
 6780                self._retreat(index)
 6781                return None
 6782
 6783            self._retreat(index2)
 6784
 6785        if not this:
 6786            assert type_token is not None
 6787            if self._match_text_seq("UNSIGNED"):
 6788                unsigned_type_token = self.SIGNED_TO_UNSIGNED_TYPE_TOKEN.get(type_token)
 6789                if not unsigned_type_token:
 6790                    self.raise_error(f"Cannot convert {type_token.name} to unsigned.")
 6791
 6792                type_token = unsigned_type_token or type_token
 6793
 6794            # NULLABLE without parentheses can be a column (Presto/Trino)
 6795            if type_token == TokenType.NULLABLE and not expressions:
 6796                self._retreat(index)
 6797                return None
 6798
 6799            this = exp.DataType(
 6800                this=exp.DType[type_token.name],
 6801                expressions=expressions,
 6802                nested=nested,
 6803            )
 6804
 6805            # Empty arrays/structs are allowed
 6806            if values is not None:
 6807                cls = exp.Struct if is_struct else exp.Array
 6808                this = exp.cast(cls(expressions=values), this, copy=False)
 6809
 6810        elif expressions:
 6811            this.set("expressions", expressions)
 6812
 6813        # https://materialize.com/docs/sql/types/list/#type-name
 6814        while self._match(TokenType.LIST):
 6815            this = exp.DataType(this=exp.DType.LIST, expressions=[this], nested=True)
 6816
 6817        index = self._index
 6818
 6819        # Postgres supports the INT ARRAY[3] syntax as a synonym for INT[3]
 6820        matched_array = self._match(TokenType.ARRAY)
 6821
 6822        while self._curr:
 6823            datatype_token = self._prev.token_type
 6824            matched_l_bracket = self._match(TokenType.L_BRACKET)
 6825
 6826            if (not matched_l_bracket and not matched_array) or (
 6827                datatype_token == TokenType.ARRAY and self._match(TokenType.R_BRACKET)
 6828            ):
 6829                # Postgres allows casting empty arrays such as ARRAY[]::INT[],
 6830                # not to be confused with the fixed size array parsing
 6831                break
 6832
 6833            matched_array = False
 6834            values = self._parse_csv(self._parse_disjunction) or None
 6835            if (
 6836                values
 6837                and not schema
 6838                and (
 6839                    not self.dialect.SUPPORTS_FIXED_SIZE_ARRAYS
 6840                    or datatype_token == TokenType.ARRAY
 6841                    or not self._match(TokenType.R_BRACKET, advance=False)
 6842                )
 6843            ):
 6844                # Retreating here means that we should not parse the following values as part of the data type, e.g. in DuckDB
 6845                # ARRAY[1] should retreat and instead be parsed into exp.Array in contrast to INT[x][y] which denotes a fixed-size array data type
 6846                self._retreat(index)
 6847                break
 6848
 6849            this = exp.DataType(
 6850                this=exp.DType.ARRAY, expressions=[this], values=values, nested=True
 6851            )
 6852            self._match(TokenType.R_BRACKET)
 6853
 6854        if self.TYPE_CONVERTERS and isinstance(this.this, exp.DType):
 6855            converter = self.TYPE_CONVERTERS.get(this.this)
 6856            if converter:
 6857                this = converter(t.cast(exp.DataType, this))
 6858
 6859        if with_collation and isinstance(this, exp.DataType) and self._match(TokenType.COLLATE):
 6860            this.set("collate", self._parse_identifier() or self._parse_column())
 6861
 6862        return this
 6863
 6864    def _parse_json_type_arg(self) -> exp.Expr | None:
 6865        """Parse a single argument to ClickHouse's JSON type."""
 6866
 6867        # SKIP col or SKIP REGEXP 'pattern'
 6868        if self._match_text_seq("SKIP"):
 6869            regexp = self._match(TokenType.RLIKE)
 6870            arg = self._parse_column()
 6871            if isinstance(arg, exp.Column):
 6872                arg = arg.to_dot()
 6873            return self.expression(exp.SkipJSONColumn(regexp=regexp, expression=arg))
 6874
 6875        param_or_col = self._parse_column()
 6876        if not isinstance(param_or_col, exp.Column):
 6877            return None
 6878
 6879        # Parameter: name=value (e.g., max_dynamic_paths=2)
 6880        if len(param_or_col.parts) == 1 and self._match(TokenType.EQ):
 6881            param = param_or_col.name
 6882            value = self._parse_primary()
 6883            return self.expression(exp.EQ(this=exp.var(param), expression=value))
 6884
 6885        # Column type hint: col_name Type
 6886        col = param_or_col.to_dot()
 6887        kind = self._parse_types(check_func=False, allow_identifiers=False)
 6888        return self.expression(exp.ColumnDef(this=col, kind=kind))
 6889
 6890    def _parse_vector_expressions(self, expressions: list[exp.Expr]) -> list[exp.Expr]:
 6891        return [exp.DataType.from_str(expressions[0].name, dialect=self.dialect), *expressions[1:]]
 6892
 6893    def _parse_struct_types(self, type_required: bool = False) -> exp.Expr | None:
 6894        index = self._index
 6895
 6896        if (
 6897            self._curr
 6898            and self._next
 6899            and self._curr.token_type in self.TYPE_TOKENS
 6900            and self._next.token_type in self.TYPE_TOKENS
 6901        ):
 6902            # Takes care of special cases like `STRUCT<list ARRAY<...>>` where the identifier is also a
 6903            # type token. Without this, the list will be parsed as a type and we'll eventually crash
 6904            this = self._parse_id_var()
 6905        else:
 6906            this = (
 6907                self._parse_type(parse_interval=False, fallback_to_identifier=True)
 6908                or self._parse_id_var()
 6909            )
 6910
 6911        self._match(TokenType.COLON)
 6912
 6913        if (
 6914            type_required
 6915            and not isinstance(this, exp.DataType)
 6916            and not self._match_set(self.TYPE_TOKENS, advance=False)
 6917        ):
 6918            self._retreat(index)
 6919            return self._parse_types()
 6920
 6921        return self._parse_column_def(this)
 6922
 6923    def _parse_at_time_zone(self, this: exp.Expr | None) -> exp.Expr | None:
 6924        if not self._match_text_seq("AT", "TIME", "ZONE"):
 6925            return this
 6926        return self._parse_at_time_zone(
 6927            self.expression(exp.AtTimeZone(this=this, zone=self._parse_unary()))
 6928        )
 6929
 6930    def _parse_atom(self) -> exp.Expr | None:
 6931        if (
 6932            self._curr.token_type in self.IDENTIFIER_TOKENS
 6933            and (column := self._parse_column()) is not None
 6934        ):
 6935            return column
 6936
 6937        token = self._curr
 6938        token_type = token.token_type
 6939
 6940        if not (primary_parser := self.PRIMARY_PARSERS.get(token_type)):
 6941            return None
 6942
 6943        next_type = self._next.token_type
 6944
 6945        if (
 6946            next_type in self.COLUMN_OPERATORS
 6947            or next_type in self.COLUMN_POSTFIX_TOKENS
 6948            or (token_type == TokenType.STRING and next_type == TokenType.STRING)
 6949        ):
 6950            return None
 6951
 6952        self._advance()
 6953        return primary_parser(self, token)
 6954
 6955    def _parse_column(self) -> exp.Expr | None:
 6956        column: exp.Expr | None = self._parse_column_parts_fast()
 6957        if column is None:
 6958            this = self._parse_column_reference()
 6959            if not this:
 6960                this = self._parse_bracket(this)
 6961            column = self._parse_column_ops(this) if this else this
 6962
 6963        if column:
 6964            if self.dialect.SUPPORTS_COLUMN_JOIN_MARKS and self._match(TokenType.JOIN_MARKER):
 6965                column.set("join_mark", True)
 6966            if self.COLON_IS_VARIANT_EXTRACT:
 6967                column = self._parse_colon_as_variant_extract(column)
 6968
 6969        return column
 6970
 6971    def _parse_column_parts_fast(self) -> exp.Column | exp.Dot | None:
 6972        """Fast path for simple column and dot references (a, a.b, ...).
 6973
 6974        Greedily consumes VAR/IDENTIFIER tokens separated by DOTs, then checks
 6975        that nothing complex follows. If it does, retreats and returns None so
 6976        the slow path can handle it. For >4 parts, wraps in exp.Dot nodes.
 6977        """
 6978        index = self._index
 6979        parts: list[exp.Identifier] | None = None
 6980        all_comments: list[str] | None = None
 6981
 6982        while self._match_set(self.IDENTIFIER_TOKENS):
 6983            token = self._prev
 6984            comments = self._prev_comments
 6985
 6986            if parts is None and token.text.upper() in self.NO_PAREN_FUNCTION_PARSERS:
 6987                self._retreat(index)
 6988                return None
 6989
 6990            has_dot = self._match(TokenType.DOT)
 6991            curr_tt = self._curr.token_type
 6992
 6993            if not has_dot:
 6994                if curr_tt in self.COLUMN_OPERATORS or curr_tt in self.COLUMN_POSTFIX_TOKENS:
 6995                    self._retreat(index)
 6996                    return None
 6997            elif curr_tt not in self.IDENTIFIER_TOKENS:
 6998                self._retreat(index)
 6999                return None
 7000
 7001            if parts is None:
 7002                parts = []
 7003
 7004            if comments:
 7005                if all_comments is None:
 7006                    all_comments = []
 7007                all_comments.extend(comments)
 7008                self._prev_comments = []
 7009
 7010            parts.append(
 7011                self.expression(
 7012                    exp.Identifier(
 7013                        this=token.text, quoted=token.token_type == TokenType.IDENTIFIER
 7014                    ),
 7015                    token,
 7016                )
 7017            )
 7018
 7019            if not has_dot:
 7020                break
 7021
 7022        if parts is None:
 7023            return None
 7024
 7025        n = len(parts)
 7026
 7027        if n == 1:
 7028            column: exp.Column | exp.Dot = exp.Column(this=parts[0])
 7029        elif n == 2:
 7030            column = exp.Column(this=parts[1], table=parts[0])
 7031        elif n == 3:
 7032            column = exp.Column(this=parts[2], table=parts[1], db=parts[0])
 7033        else:
 7034            column = exp.Column(this=parts[3], table=parts[2], db=parts[1], catalog=parts[0])
 7035
 7036            for i in range(4, n):
 7037                column = exp.Dot(this=column, expression=parts[i])
 7038
 7039        if all_comments:
 7040            column.add_comments(all_comments)
 7041
 7042        return column
 7043
 7044    def _parse_column_reference(self) -> exp.Expr | None:
 7045        this = self._parse_field()
 7046        if (
 7047            not this
 7048            and self._match(TokenType.VALUES, advance=False)
 7049            and self.VALUES_FOLLOWED_BY_PAREN
 7050            and (not self._next or self._next.token_type != TokenType.L_PAREN)
 7051        ):
 7052            this = self._parse_id_var()
 7053
 7054        if isinstance(this, exp.Identifier):
 7055            # We bubble up comments from the Identifier to the Column
 7056            this = self.expression(exp.Column(this=this), comments=this.pop_comments())
 7057
 7058        return this
 7059
 7060    def _build_json_extract(
 7061        self,
 7062        this: exp.Expr | None,
 7063        path_parts: list[exp.JSONPathPart],
 7064    ) -> tuple[exp.Expr | None, list[exp.JSONPathPart]]:
 7065        if len(path_parts) > 1:
 7066            this = self.expression(
 7067                exp.JSONExtract(
 7068                    this=this,
 7069                    expression=exp.JSONPath(expressions=path_parts),
 7070                    variant_extract=True,
 7071                    requires_json=self.JSON_EXTRACT_REQUIRES_JSON_EXPRESSION,
 7072                )
 7073            )
 7074            path_parts = [exp.JSONPathRoot()]
 7075
 7076        return this, path_parts
 7077
 7078    def _parse_colon_as_variant_extract(self, this: exp.Expr | None) -> exp.Expr | None:
 7079        path_parts: list[exp.JSONPathPart] = [exp.JSONPathRoot()]
 7080
 7081        while self._match(TokenType.COLON):
 7082            if not self.COLON_CHAIN_IS_SINGLE_EXTRACT:
 7083                this, path_parts = self._build_json_extract(this, path_parts)
 7084
 7085            key = self._parse_id_var(any_token=True, tokens=(TokenType.SELECT,))
 7086
 7087            if key:
 7088                quoted = isinstance(key, exp.Identifier) and key.quoted
 7089                path_parts.append(exp.JSONPathKey(this=key.name, quoted=quoted))
 7090
 7091            while True:
 7092                if self._match(TokenType.DOT):
 7093                    next_key = self._parse_id_var(any_token=True, tokens=(TokenType.SELECT,))
 7094
 7095                    if next_key:
 7096                        quoted = isinstance(next_key, exp.Identifier) and next_key.quoted
 7097                        path_parts.append(exp.JSONPathKey(this=next_key.name, quoted=quoted))
 7098                elif self._match(TokenType.L_BRACKET):
 7099                    bracket_expr = self._parse_bracket_key_value()
 7100
 7101                    if not self._match(TokenType.R_BRACKET):
 7102                        self.raise_error("Expected ]")
 7103
 7104                    if bracket_expr:
 7105                        if bracket_expr.is_string:
 7106                            path_parts.append(exp.JSONPathKey(this=bracket_expr.name, quoted=True))
 7107                        elif bracket_expr.is_star:
 7108                            path_parts.append(exp.JSONPathSubscript(this=exp.JSONPathWildcard()))
 7109                        elif bracket_expr.is_number:
 7110                            path_parts.append(exp.JSONPathSubscript(this=bracket_expr.to_py()))
 7111                        else:
 7112                            this, path_parts = self._build_json_extract(this, path_parts)
 7113
 7114                            this = self.expression(
 7115                                exp.Bracket(
 7116                                    this=this, expressions=[bracket_expr], json_access=True
 7117                                ),
 7118                            )
 7119
 7120                elif self._match(TokenType.DCOLON):
 7121                    this, path_parts = self._build_json_extract(this, path_parts)
 7122
 7123                    cast_type = self._parse_types()
 7124                    if cast_type:
 7125                        this = self.expression(exp.Cast(this=this, to=cast_type))
 7126                    else:
 7127                        self.raise_error("Expected type after '::'")
 7128                else:
 7129                    break
 7130
 7131        this, _ = self._build_json_extract(this, path_parts)
 7132
 7133        return this
 7134
 7135    def _parse_dcolon(self) -> exp.Expr | None:
 7136        return self._parse_types()
 7137
 7138    def _parse_column_ops(self, this: exp.Expr | None) -> exp.Expr | None:
 7139        while self._curr.token_type in self.BRACKETS:
 7140            this = self._parse_bracket(this)
 7141
 7142        column_operators = self.COLUMN_OPERATORS
 7143        cast_column_operators = self.CAST_COLUMN_OPERATORS
 7144        while self._curr:
 7145            op_token = self._curr.token_type
 7146
 7147            if op_token not in column_operators:
 7148                break
 7149            op = column_operators[op_token]
 7150            self._advance()
 7151
 7152            if op_token in cast_column_operators:
 7153                field = self._parse_dcolon()
 7154                if not field:
 7155                    self.raise_error("Expected type")
 7156            elif op and self._curr:
 7157                field = self._parse_column_reference() or self._parse_bitwise()
 7158                if isinstance(field, exp.Column) and self._match(TokenType.DOT, advance=False):
 7159                    field = self._parse_column_ops(field)
 7160            else:
 7161                dot = self._is_connected() and self._prev.token_type == TokenType.DOT
 7162                field = self._parse_field(any_token=True, anonymous_func=True)
 7163
 7164                # In t.true, t.null we should produce an Identifier node
 7165                if dot and isinstance(field, (exp.Null, exp.Boolean)):
 7166                    field = self.expression(
 7167                        exp.Identifier(this=self._prev.text),
 7168                        comments=field.comments,
 7169                    )
 7170
 7171            # Function calls can be qualified, e.g., x.y.FOO()
 7172            # This converts the final AST to a series of Dots leading to the function call
 7173            # https://cloud.google.com/bigquery/docs/reference/standard-sql/functions-reference#function_call_rules
 7174            if isinstance(field, (exp.Func, exp.Window)) and this:
 7175                this = this.transform(
 7176                    lambda n: n.to_dot(include_dots=False) if isinstance(n, exp.Column) else n
 7177                )
 7178
 7179            if op:
 7180                this = op(self, this, field)
 7181            elif isinstance(this, exp.Column) and not this.args.get("catalog"):
 7182                this = self.expression(
 7183                    exp.Column(
 7184                        this=field,
 7185                        table=this.this,
 7186                        db=this.args.get("table"),
 7187                        catalog=this.args.get("db"),
 7188                    ),
 7189                    comments=this.comments,
 7190                )
 7191            elif isinstance(field, exp.Window):
 7192                # Move the exp.Dot's to the window's function
 7193                window_func = self.expression(exp.Dot(this=this, expression=field.this))
 7194                field.set("this", window_func)
 7195                this = field
 7196            else:
 7197                this = self.expression(exp.Dot(this=this, expression=field))
 7198
 7199            if field and field.comments:
 7200                t.cast(exp.Expr, this).add_comments(field.pop_comments())
 7201
 7202            this = self._parse_bracket(this)
 7203
 7204        return this
 7205
 7206    def _parse_paren(self) -> exp.Expr | None:
 7207        if not self._match(TokenType.L_PAREN):
 7208            return None
 7209
 7210        comments = self._prev_comments
 7211        query = self._parse_select()
 7212
 7213        if query:
 7214            expressions = [query]
 7215        else:
 7216            expressions = self._parse_expressions()
 7217
 7218        this = seq_get(expressions, 0)
 7219
 7220        if not this and self._match(TokenType.R_PAREN, advance=False):
 7221            this = self.expression(exp.Tuple())
 7222        elif len(expressions) > 1 or self._prev.token_type == TokenType.COMMA:
 7223            this = self.expression(exp.Tuple(expressions=expressions))
 7224        elif isinstance(this, exp.UNWRAPPED_QUERIES):
 7225            this = self._parse_subquery(this=this, parse_alias=False)
 7226        elif isinstance(this, (exp.Subquery, exp.Values)):
 7227            this = self._parse_subquery(
 7228                this=self._parse_query_modifiers(self._parse_set_operations(this)),
 7229                parse_alias=False,
 7230            )
 7231        else:
 7232            this = self.expression(exp.Paren(this=this))
 7233
 7234        if this:
 7235            this.add_comments(comments)
 7236
 7237        self._match_r_paren(expression=this)
 7238
 7239        if isinstance(this, exp.Paren) and isinstance(this.this, exp.AggFunc):
 7240            return self._parse_window(this)
 7241
 7242        return this
 7243
 7244    def _parse_primary(self) -> exp.Expr | None:
 7245        if self._match_set(self.PRIMARY_PARSERS):
 7246            token_type = self._prev.token_type
 7247            primary = self.PRIMARY_PARSERS[token_type](self, self._prev)
 7248
 7249            if token_type == TokenType.STRING:
 7250                expressions = [primary]
 7251                while self._match(TokenType.STRING, advance=False):
 7252                    if self._is_connected() and self.ADJACENT_STRINGS_CANNOT_BE_CONNECTED:
 7253                        self.raise_error(
 7254                            "Adjacent string literals need to be separated by whitespace or comments"
 7255                        )
 7256
 7257                    self._advance()
 7258                    expressions.append(exp.Literal.string(self._prev.text))
 7259
 7260                if len(expressions) > 1:
 7261                    return self.expression(
 7262                        exp.Concat(expressions=expressions, coalesce=self.dialect.CONCAT_COALESCE)
 7263                    )
 7264
 7265            return primary
 7266
 7267        if self._match_pair(TokenType.DOT, TokenType.NUMBER):
 7268            return exp.Literal.number(f"0.{self._prev.text}")
 7269
 7270        return self._parse_paren()
 7271
 7272    def _parse_field(
 7273        self,
 7274        any_token: bool = False,
 7275        tokens: t.Collection[TokenType] | None = None,
 7276        anonymous_func: bool = False,
 7277    ) -> exp.Expr | None:
 7278        after_dot = (
 7279            self.SUPPORTS_DIGIT_PREFIXED_FIELD_NAMES and self._prev.token_type == TokenType.DOT
 7280        )
 7281
 7282        if anonymous_func:
 7283            field = (
 7284                self._parse_function(anonymous=anonymous_func, any_token=any_token)
 7285                or self._parse_primary()
 7286            )
 7287        else:
 7288            field = self._parse_primary() or self._parse_function(
 7289                anonymous=anonymous_func, any_token=any_token
 7290            )
 7291
 7292        field = field or self._parse_id_var(any_token=any_token, tokens=tokens)
 7293
 7294        if after_dot and isinstance(field, exp.Literal) and field.is_number:
 7295            name = field.name
 7296            if self._is_connected() and self._parse_var(any_token=True):
 7297                name += self._prev.text
 7298
 7299            field = exp.Identifier(this=name, quoted=True).update_positions(field)
 7300
 7301        return field
 7302
 7303    def _parse_function(
 7304        self,
 7305        functions: dict[str, t.Callable] | None = None,
 7306        anonymous: bool = False,
 7307        optional_parens: bool = True,
 7308        any_token: bool = False,
 7309    ) -> exp.Expr | None:
 7310        # This allows us to also parse {fn <function>} syntax (Snowflake, MySQL support this)
 7311        # See: https://community.snowflake.com/s/article/SQL-Escape-Sequences
 7312        fn_syntax = False
 7313        if (
 7314            self._match(TokenType.L_BRACE, advance=False)
 7315            and self._next
 7316            and self._next.text.upper() == "FN"
 7317        ):
 7318            self._advance(2)
 7319            fn_syntax = True
 7320
 7321        func = self._parse_function_call(
 7322            functions=functions,
 7323            anonymous=anonymous,
 7324            optional_parens=optional_parens,
 7325            any_token=any_token,
 7326        )
 7327
 7328        if fn_syntax:
 7329            self._match(TokenType.R_BRACE)
 7330
 7331        return func
 7332
 7333    def _parse_function_args(self, alias: bool = False) -> list[exp.Expr]:
 7334        return self._parse_csv(lambda: self._parse_lambda(alias=alias))
 7335
 7336    def _parse_connector_function(self, connector: t.Callable[..., exp.Condition]) -> exp.Paren:
 7337        args = self._parse_function_args(alias=False)
 7338        if not args:
 7339            self.raise_error("Expected at least one argument")
 7340
 7341        # Wrapped so the connector keeps its precedence in the parent context
 7342        return exp.Paren(this=connector(*args, copy=False))
 7343
 7344    def _parse_function_call(
 7345        self,
 7346        functions: dict[str, t.Callable] | None = None,
 7347        anonymous: bool = False,
 7348        optional_parens: bool = True,
 7349        any_token: bool = False,
 7350    ) -> exp.Expr | None:
 7351        if not self._curr:
 7352            return None
 7353
 7354        comments = self._curr.comments
 7355        prev = self._prev
 7356        token = self._curr
 7357        token_type = self._curr.token_type
 7358        this: str | exp.Expr = self._curr.text
 7359        upper = self._curr.text.upper()
 7360
 7361        after_dot = prev.token_type == TokenType.DOT
 7362        parser = self.NO_PAREN_FUNCTION_PARSERS.get(upper)
 7363        if (
 7364            optional_parens
 7365            and parser
 7366            and token_type not in self.INVALID_FUNC_NAME_TOKENS
 7367            and not after_dot
 7368        ):
 7369            self._advance()
 7370            return self._parse_window(parser(self))
 7371
 7372        if self._next.token_type != TokenType.L_PAREN:
 7373            if optional_parens and token_type in self.NO_PAREN_FUNCTIONS and not after_dot:
 7374                self._advance()
 7375                return self.expression(self.NO_PAREN_FUNCTIONS[token_type]())
 7376
 7377            return None
 7378
 7379        if any_token:
 7380            if token_type in self.RESERVED_TOKENS:
 7381                return None
 7382        elif token_type not in self.FUNC_TOKENS:
 7383            return None
 7384
 7385        self._advance(2)
 7386
 7387        parser = self.FUNCTION_PARSERS.get(upper)
 7388        if parser and not anonymous:
 7389            result = parser(self)
 7390        else:
 7391            subquery_predicate = self.SUBQUERY_PREDICATES.get(token_type)
 7392
 7393            if subquery_predicate:
 7394                expr = None
 7395                if self._curr.token_type in self.SUBQUERY_TOKENS:
 7396                    expr = self._parse_select()
 7397                    self._match_r_paren()
 7398                elif prev and prev.token_type in (TokenType.LIKE, TokenType.ILIKE):
 7399                    # Backtrack one token since we've consumed the L_PAREN here. Instead, we'd like
 7400                    # to parse "LIKE [ANY | ALL] (...)" as a whole into an exp.Tuple or exp.Paren
 7401                    self._advance(-1)
 7402                    expr = self._parse_bitwise()
 7403
 7404                if expr:
 7405                    return self.expression(subquery_predicate(this=expr), comments=comments)
 7406
 7407            if functions is None:
 7408                functions = self.FUNCTIONS
 7409
 7410            function = functions.get(upper)
 7411            known_function = function and not anonymous
 7412
 7413            alias = not known_function or upper in self.FUNCTIONS_WITH_ALIASED_ARGS
 7414            args = self._parse_function_args(alias)
 7415
 7416            post_func_comments = self._curr.comments if self._curr else None
 7417            if known_function and post_func_comments:
 7418                # If the user-inputted comment "/* sqlglot.anonymous */" is following the function
 7419                # call we'll construct it as exp.Anonymous, even if it's "known"
 7420                if any(
 7421                    comment.lstrip().startswith(exp.SQLGLOT_ANONYMOUS)
 7422                    for comment in post_func_comments
 7423                ):
 7424                    known_function = False
 7425
 7426            if alias and known_function:
 7427                args = self._kv_to_prop_eq(args)
 7428
 7429            if known_function:
 7430                func_builder = t.cast(t.Callable, function)
 7431
 7432                # mypyc compiled functions don't have __code__, so we use
 7433                # try/except to check if func_builder accepts 'dialect'.
 7434                try:
 7435                    func = func_builder(args)
 7436                except TypeError:
 7437                    func = func_builder(args, dialect=self.dialect)
 7438
 7439                func = self.validate_expression(func, args)
 7440                if name_key := self.dialect.ORIGINAL_NAME_META_KEY:
 7441                    func.meta[name_key] = this
 7442
 7443                result = func
 7444            else:
 7445                if token_type == TokenType.IDENTIFIER:
 7446                    this = exp.Identifier(this=this, quoted=True).update_positions(token)
 7447
 7448                result = self.expression(exp.Anonymous(this=this, expressions=args))
 7449
 7450            result = result.update_positions(token)
 7451
 7452        if isinstance(result, exp.Expr):
 7453            result.add_comments(comments)
 7454
 7455        if parser:
 7456            self._match(TokenType.R_PAREN, expression=result)
 7457        else:
 7458            self._match_r_paren(result)
 7459        return self._parse_window(result)
 7460
 7461    def _to_prop_eq(self, expression: exp.Expr, index: int) -> exp.Expr:
 7462        return expression
 7463
 7464    def _kv_to_prop_eq(
 7465        self, expressions: list[exp.Expr], parse_map: bool = False
 7466    ) -> list[exp.Expr]:
 7467        transformed = []
 7468
 7469        for index, e in enumerate(expressions):
 7470            if isinstance(e, self.KEY_VALUE_DEFINITIONS):
 7471                if isinstance(e, exp.Alias):
 7472                    e = self.expression(exp.PropertyEQ(this=e.args.get("alias"), expression=e.this))
 7473
 7474                if not isinstance(e, exp.PropertyEQ):
 7475                    e = self.expression(
 7476                        exp.PropertyEQ(
 7477                            this=e.this if parse_map else exp.to_identifier(e.this.name),
 7478                            expression=e.expression,
 7479                        )
 7480                    )
 7481
 7482                if isinstance(e.this, exp.Column):
 7483                    e.this.replace(e.this.this)
 7484            else:
 7485                e = self._to_prop_eq(e, index)
 7486
 7487            transformed.append(e)
 7488
 7489        return transformed
 7490
 7491    def _parse_function_properties(self) -> exp.Properties | None:
 7492        # Skip the generic `key = value` fallback in _parse_property since this
 7493        # runs post-AS where a function body like `name = expr` can be misread
 7494        # as a property.
 7495        properties = []
 7496        while True:
 7497            if self._match_texts(self.PROPERTY_PARSERS):
 7498                keyword = self._prev.text.upper()
 7499                prop = self.PROPERTY_PARSERS[keyword](self)
 7500            elif self._match(TokenType.DEFAULT) and self._match_texts(self.PROPERTY_PARSERS):
 7501                keyword = self._prev.text.upper()
 7502                prop = self.PROPERTY_PARSERS[keyword](self, default=True)
 7503            else:
 7504                break
 7505            if not prop:
 7506                self.raise_error(f"Failed to parse property '{keyword}'")
 7507                break
 7508            for p in ensure_list(prop):
 7509                properties.append(p)
 7510
 7511        return self.expression(exp.Properties(expressions=properties)) if properties else None
 7512
 7513    def _parse_user_defined_function_expression(self) -> exp.Expr | None:
 7514        return self._parse_statement()
 7515
 7516    def _parse_function_parameter(self) -> exp.Expr | None:
 7517        return self._parse_column_def(this=self._parse_id_var(), computed_column=False)
 7518
 7519    def _parse_user_defined_function(self, kind: TokenType | None = None) -> exp.Expr | None:
 7520        this = self._parse_table_parts(schema=True)
 7521
 7522        if not self._match(TokenType.L_PAREN):
 7523            return this
 7524
 7525        expressions = self._parse_csv(self._parse_function_parameter)
 7526        self._match_r_paren()
 7527        return self.expression(
 7528            exp.UserDefinedFunction(this=this, expressions=expressions, wrapped=True)
 7529        )
 7530
 7531    def _parse_macro_overloads(
 7532        self,
 7533        this: exp.UserDefinedFunction,
 7534        first_body: exp.Expr,
 7535        first_is_table: bool = False,
 7536    ) -> exp.MacroOverloads:
 7537        overloads = [
 7538            self.expression(
 7539                exp.MacroOverload(
 7540                    this=first_body,
 7541                    expressions=this.expressions or None,
 7542                    is_table=first_is_table,
 7543                )
 7544            )
 7545        ]
 7546        this.set("expressions", None)
 7547        this.set("wrapped", False)
 7548
 7549        while self._match(TokenType.COMMA):
 7550            if not self._match(TokenType.L_PAREN):
 7551                break
 7552
 7553            params = self._parse_csv(self._parse_function_parameter)
 7554            self._match_r_paren()
 7555
 7556            if not self._match(TokenType.ALIAS):
 7557                break
 7558
 7559            is_table = self._match(TokenType.TABLE)
 7560            body = self._parse_expression()
 7561            macro = exp.MacroOverload(this=body, expressions=params, is_table=is_table)
 7562            overloads.append(self.expression(macro))
 7563
 7564        return self.expression(exp.MacroOverloads(expressions=overloads))
 7565
 7566    def _parse_introducer(self, token: Token) -> exp.Introducer | exp.Identifier:
 7567        literal = self._parse_primary()
 7568        if literal:
 7569            return self.expression(exp.Introducer(this=token.text, expression=literal), token)
 7570
 7571        return self._identifier_expression(token)
 7572
 7573    def _parse_session_parameter(self) -> exp.SessionParameter:
 7574        kind = None
 7575        this = self._parse_id_var() or self._parse_primary()
 7576
 7577        if this and self._match(TokenType.DOT):
 7578            kind = this.name
 7579            this = self._parse_var() or self._parse_primary()
 7580
 7581        return self.expression(exp.SessionParameter(this=this, kind=kind))
 7582
 7583    def _parse_lambda_arg(self) -> exp.Expr | None:
 7584        return self._parse_id_var()
 7585
 7586    def _parse_lambda(self, alias: bool = False) -> exp.Expr | None:
 7587        next_token_type = self._next.token_type
 7588
 7589        # Fast path: simple atom (column, literal, null, bool) followed by , or )
 7590        if (
 7591            next_token_type in self.LAMBDA_ARG_TERMINATORS
 7592            and (atom := self._parse_atom()) is not None
 7593        ):
 7594            return atom
 7595
 7596        index = self._index
 7597
 7598        if self._match(TokenType.L_PAREN):
 7599            expressions = t.cast(
 7600                list[t.Optional[exp.Expr]], self._parse_csv(self._parse_lambda_arg)
 7601            )
 7602
 7603            if not self._match(TokenType.R_PAREN):
 7604                self._retreat(index)
 7605            elif self._match_set(self.LAMBDAS):
 7606                return self.LAMBDAS[self._prev.token_type](self, expressions)
 7607            else:
 7608                self._retreat(index)
 7609        elif self.TYPED_LAMBDA_ARGS or next_token_type in self.LAMBDAS:
 7610            expressions = [self._parse_lambda_arg()]
 7611
 7612            if self._match_set(self.LAMBDAS):
 7613                return self.LAMBDAS[self._prev.token_type](self, expressions)
 7614
 7615            self._retreat(index)
 7616
 7617        this: exp.Expr | None
 7618
 7619        if self._match(TokenType.DISTINCT):
 7620            this = self.expression(
 7621                exp.Distinct(expressions=self._parse_csv(self._parse_disjunction))
 7622            )
 7623        else:
 7624            self._match(TokenType.ALL)  # ALL is the default/no-op aggregate modifier (SQL-92)
 7625            this = self._parse_select_or_expression(alias=alias)
 7626
 7627        return self._parse_limit(
 7628            self._parse_respect_or_ignore_nulls(
 7629                self._parse_order(self._parse_having_max(self._parse_respect_or_ignore_nulls(this)))
 7630            )
 7631        )
 7632
 7633    def _parse_schema(self, this: exp.Expr | None = None) -> exp.Expr | None:
 7634        index = self._index
 7635        if not self._match(TokenType.L_PAREN):
 7636            return this
 7637
 7638        # Disambiguate between schema and subquery/CTE, e.g. in INSERT INTO table (<expr>),
 7639        # expr can be of both types
 7640        if self._match_set(self.SELECT_START_TOKENS):
 7641            self._retreat(index)
 7642            return this
 7643        args = self._parse_csv(lambda: self._parse_constraint() or self._parse_field_def())
 7644        self._match_r_paren()
 7645        return self.expression(exp.Schema(this=this, expressions=args))
 7646
 7647    def _parse_field_def(self) -> exp.Expr | None:
 7648        return self._parse_column_def(self._parse_field(any_token=True))
 7649
 7650    def _parse_column_def(
 7651        self, this: exp.Expr | None, computed_column: bool = True
 7652    ) -> exp.Expr | None:
 7653        # column defs are not really columns, they're identifiers
 7654        if isinstance(this, exp.Column):
 7655            this = this.this
 7656
 7657        if not computed_column:
 7658            self._match(TokenType.ALIAS)
 7659
 7660        kind = self._parse_types(schema=True)
 7661
 7662        if self._match_text_seq("FOR", "ORDINALITY"):
 7663            return self.expression(exp.ColumnDef(this=this, ordinality=True))
 7664
 7665        constraints: list[exp.Expr] = []
 7666
 7667        if (not kind and self._match(TokenType.ALIAS)) or self._match_texts(
 7668            ("ALIAS", "MATERIALIZED")
 7669        ):
 7670            # Match storage before _parse_types so STORED is not treated as a data type
 7671            # (needed for typeless columns, e.g. SQLite `b AS (a * 2) STORED`).
 7672            persisted = self._prev.text.upper() == "MATERIALIZED"
 7673            expression = self._parse_disjunction()
 7674            if not persisted:
 7675                if self._match_text_seq("PERSISTED"):
 7676                    persisted = True
 7677                elif self._match_texts(("STORED", "VIRTUAL")):
 7678                    persisted = self._prev.text.upper() == "STORED"
 7679            constraint_kind = exp.ComputedColumnConstraint(
 7680                this=expression,
 7681                persisted=persisted,
 7682                data_type=exp.Var(this="AUTO")
 7683                if self._match_text_seq("AUTO")
 7684                else self._parse_types(),
 7685                not_null=self._match_pair(TokenType.NOT, TokenType.NULL),
 7686            )
 7687            constraints.append(self.expression(exp.ColumnConstraint(kind=constraint_kind)))
 7688        elif not kind and self._match_set({TokenType.IN, TokenType.OUT}, advance=False):
 7689            in_out_constraint = self.expression(
 7690                exp.InOutColumnConstraint(
 7691                    input_=self._match(TokenType.IN), output=self._match(TokenType.OUT)
 7692                )
 7693            )
 7694            constraints.append(in_out_constraint)
 7695            kind = self._parse_types()
 7696        elif (
 7697            kind
 7698            and self._match(TokenType.ALIAS, advance=False)
 7699            and (
 7700                not self.WRAPPED_TRANSFORM_COLUMN_CONSTRAINT
 7701                or self._next.token_type == TokenType.L_PAREN
 7702            )
 7703        ):
 7704            self._advance()
 7705            constraints.append(
 7706                self.expression(
 7707                    exp.ColumnConstraint(
 7708                        kind=exp.ComputedColumnConstraint(
 7709                            this=self._parse_disjunction(),
 7710                            persisted=self._match_texts(("STORED", "VIRTUAL"))
 7711                            and self._prev.text.upper() == "STORED",
 7712                        )
 7713                    )
 7714                )
 7715            )
 7716
 7717        while True:
 7718            constraint = self._parse_column_constraint()
 7719            if not constraint:
 7720                break
 7721            constraints.append(constraint)
 7722
 7723        if not kind and not constraints:
 7724            return this
 7725
 7726        position = None
 7727        if self._match_texts(("FIRST", "AFTER")):
 7728            pos = self._prev.text
 7729            position = self.expression(exp.ColumnPosition(this=self._parse_column(), position=pos))
 7730
 7731        return self.expression(
 7732            exp.ColumnDef(this=this, kind=kind, constraints=constraints, position=position)
 7733        )
 7734
 7735    def _parse_auto_increment(
 7736        self,
 7737    ) -> exp.GeneratedAsIdentityColumnConstraint | exp.AutoIncrementColumnConstraint:
 7738        start = None
 7739        increment = None
 7740        order = None
 7741
 7742        if self._match(TokenType.L_PAREN, advance=False):
 7743            args = self._parse_wrapped_csv(self._parse_bitwise)
 7744            start = seq_get(args, 0)
 7745            increment = seq_get(args, 1)
 7746
 7747        # The remaining parts form an unordered bag and any of them can be omitted, in which
 7748        # case the engine falls back to its own default, so they're parsed independently.
 7749        while True:
 7750            if self._match_text_seq("START"):
 7751                start = self._parse_bitwise()
 7752            elif self._match_text_seq("INCREMENT"):
 7753                increment = self._parse_bitwise()
 7754            elif self._match_text_seq("ORDER"):
 7755                order = True
 7756            elif self._match_text_seq("NOORDER"):
 7757                order = False
 7758            else:
 7759                break
 7760
 7761        if start or increment or order is not None:
 7762            return exp.GeneratedAsIdentityColumnConstraint(
 7763                start=start, increment=increment, this=False, order=order
 7764            )
 7765
 7766        return exp.AutoIncrementColumnConstraint()
 7767
 7768    def _parse_check_constraint(self) -> exp.CheckColumnConstraint | None:
 7769        if not self._match(TokenType.L_PAREN, advance=False):
 7770            return None
 7771
 7772        return self.expression(
 7773            exp.CheckColumnConstraint(
 7774                this=self._parse_wrapped(self._parse_assignment),
 7775                enforced=self._match_text_seq("ENFORCED"),
 7776            )
 7777        )
 7778
 7779    def _parse_auto_property(self) -> exp.AutoRefreshProperty | None:
 7780        if not self._match_text_seq("REFRESH"):
 7781            self._retreat(self._index - 1)
 7782            return None
 7783        return self.expression(exp.AutoRefreshProperty(this=self._parse_var(upper=True)))
 7784
 7785    def _parse_compress(self) -> exp.CompressColumnConstraint:
 7786        if self._match(TokenType.L_PAREN, advance=False):
 7787            return self.expression(
 7788                exp.CompressColumnConstraint(this=self._parse_wrapped_csv(self._parse_bitwise))
 7789            )
 7790
 7791        return self.expression(exp.CompressColumnConstraint(this=self._parse_bitwise()))
 7792
 7793    def _parse_generated_as_identity(
 7794        self,
 7795    ) -> (
 7796        exp.GeneratedAsIdentityColumnConstraint
 7797        | exp.ComputedColumnConstraint
 7798        | exp.GeneratedAsRowColumnConstraint
 7799    ):
 7800        if self._match_text_seq("BY", "DEFAULT"):
 7801            on_null = self._match_pair(TokenType.ON, TokenType.NULL)
 7802            this = self.expression(
 7803                exp.GeneratedAsIdentityColumnConstraint(this=False, on_null=on_null)
 7804            )
 7805        else:
 7806            self._match_text_seq("ALWAYS")
 7807            this = self.expression(exp.GeneratedAsIdentityColumnConstraint(this=True))
 7808
 7809        self._match(TokenType.ALIAS)
 7810
 7811        if self._match_text_seq("ROW"):
 7812            start = self._match_text_seq("START")
 7813            if not start:
 7814                self._match(TokenType.END)
 7815            hidden = self._match_text_seq("HIDDEN")
 7816            return self.expression(exp.GeneratedAsRowColumnConstraint(start=start, hidden=hidden))
 7817
 7818        identity = self._match_text_seq("IDENTITY")
 7819
 7820        if self._match(TokenType.L_PAREN):
 7821            if self._match_text_seq("START", "WITH"):
 7822                this.set("start", self._parse_bitwise())
 7823            if self._match_text_seq("INCREMENT", "BY"):
 7824                this.set("increment", self._parse_bitwise())
 7825            if self._match_text_seq("MINVALUE"):
 7826                this.set("minvalue", self._parse_bitwise())
 7827            if self._match_text_seq("MAXVALUE"):
 7828                this.set("maxvalue", self._parse_bitwise())
 7829
 7830            if self._match_text_seq("CYCLE"):
 7831                this.set("cycle", True)
 7832            elif self._match_text_seq("NO", "CYCLE"):
 7833                this.set("cycle", False)
 7834
 7835            if not identity:
 7836                this.set("expression", self._parse_range())
 7837            elif not this.args.get("start") and self._match(TokenType.NUMBER, advance=False):
 7838                args = self._parse_csv(self._parse_bitwise)
 7839                this.set("start", seq_get(args, 0))
 7840                this.set("increment", seq_get(args, 1))
 7841
 7842            self._match_r_paren()
 7843
 7844        return this
 7845
 7846    def _parse_inline(self) -> exp.InlineLengthColumnConstraint:
 7847        self._match_text_seq("LENGTH")
 7848        return self.expression(exp.InlineLengthColumnConstraint(this=self._parse_bitwise()))
 7849
 7850    def _parse_not_constraint(self) -> exp.Expr | None:
 7851        if self._match_text_seq("NULL"):
 7852            return self.expression(exp.NotNullColumnConstraint())
 7853        if self._match_text_seq("CASESPECIFIC"):
 7854            return self.expression(exp.CaseSpecificColumnConstraint(not_=True))
 7855        if self._match_text_seq("FOR", "REPLICATION"):
 7856            return self.expression(exp.NotForReplicationColumnConstraint())
 7857
 7858        # Unconsume the `NOT` token
 7859        self._retreat(self._index - 1)
 7860        return None
 7861
 7862    def _parse_column_constraint(self) -> exp.Expr | None:
 7863        this = self._parse_id_var() if self._match(TokenType.CONSTRAINT) else None
 7864
 7865        procedure_option_follows = (
 7866            self._match(TokenType.WITH, advance=False)
 7867            and self._next
 7868            and self._next.text.upper() in self.PROCEDURE_OPTIONS
 7869        )
 7870
 7871        index = self._index
 7872        if not procedure_option_follows and self._match_texts(self.CONSTRAINT_PARSERS):
 7873            constraint = self.CONSTRAINT_PARSERS[self._prev.text.upper()](self)
 7874            if not constraint:
 7875                self._retreat(index)
 7876                return None
 7877
 7878            return self.expression(exp.ColumnConstraint(this=this, kind=constraint))
 7879
 7880        if self._match(TokenType.CHARACTER_SET) or self._match_pair(TokenType.CHAR, TokenType.SET):
 7881            return self.expression(
 7882                exp.ColumnConstraint(
 7883                    this=this,
 7884                    kind=self.expression(
 7885                        exp.CharacterSetColumnConstraint(this=self._parse_var_or_string())
 7886                    ),
 7887                )
 7888            )
 7889
 7890        return this
 7891
 7892    def _parse_constraint(self) -> exp.Expr | None:
 7893        if not self._match(TokenType.CONSTRAINT):
 7894            return self._parse_unnamed_constraint(constraints=self.SCHEMA_UNNAMED_CONSTRAINTS)
 7895
 7896        return self.expression(
 7897            exp.Constraint(this=self._parse_id_var(), expressions=self._parse_unnamed_constraints())
 7898        )
 7899
 7900    def _parse_unnamed_constraints(self) -> list[exp.Expr]:
 7901        constraints = []
 7902        while True:
 7903            constraint = self._parse_unnamed_constraint() or self._parse_function()
 7904            if not constraint:
 7905                break
 7906            constraints.append(constraint)
 7907
 7908        return constraints
 7909
 7910    def _parse_unnamed_constraint(self, constraints: TEXTS_TYPE | None = None) -> exp.Expr | None:
 7911        index = self._index
 7912
 7913        if self._match(TokenType.IDENTIFIER, advance=False) or not self._match_texts(
 7914            constraints or self.CONSTRAINT_PARSERS
 7915        ):
 7916            return None
 7917
 7918        constraint_key = self._prev.text.upper()
 7919        if constraint_key not in self.CONSTRAINT_PARSERS:
 7920            self.raise_error(f"No parser found for schema constraint {constraint_key}.")
 7921
 7922        result = self.CONSTRAINT_PARSERS[constraint_key](self)
 7923        if not result:
 7924            self._retreat(index)
 7925
 7926        return result
 7927
 7928    def _parse_unique_key(self) -> exp.Expr | None:
 7929        if (
 7930            self._curr
 7931            and self._curr.token_type != TokenType.IDENTIFIER
 7932            and self._curr.text.upper() in self.CONSTRAINT_PARSERS
 7933        ):
 7934            return None
 7935        return self._parse_id_var(any_token=False)
 7936
 7937    def _parse_unique(self) -> exp.UniqueColumnConstraint:
 7938        self._match_texts(("KEY", "INDEX"))
 7939        return self.expression(
 7940            exp.UniqueColumnConstraint(
 7941                nulls=self._match_text_seq("NULLS", "NOT", "DISTINCT"),
 7942                this=self._parse_schema(self._parse_unique_key()),
 7943                index_type=self._match(TokenType.USING) and self._advance_any() and self._prev.text,
 7944                on_conflict=self._parse_on_conflict(),
 7945                options=self._parse_key_constraint_options(),
 7946            )
 7947        )
 7948
 7949    def _parse_key_constraint_options(self) -> list[str]:
 7950        options = []
 7951        while True:
 7952            if not self._curr:
 7953                break
 7954
 7955            if self._match(TokenType.ON):
 7956                action = None
 7957                on = self._advance_any() and self._prev.text
 7958
 7959                if self._match_text_seq("NO", "ACTION"):
 7960                    action = "NO ACTION"
 7961                elif self._match_text_seq("CASCADE"):
 7962                    action = "CASCADE"
 7963                elif self._match_text_seq("RESTRICT"):
 7964                    action = "RESTRICT"
 7965                elif self._match_pair(TokenType.SET, TokenType.NULL):
 7966                    action = "SET NULL"
 7967                elif self._match_pair(TokenType.SET, TokenType.DEFAULT):
 7968                    action = "SET DEFAULT"
 7969                else:
 7970                    self.raise_error("Invalid key constraint")
 7971
 7972                options.append(f"ON {on} {action}")
 7973            else:
 7974                var = self._parse_var_from_options(
 7975                    self.KEY_CONSTRAINT_OPTIONS, raise_unmatched=False
 7976                )
 7977                if not var:
 7978                    break
 7979                options.append(var.name)
 7980
 7981        return options
 7982
 7983    def _parse_references(self, match: bool = True) -> exp.Reference | None:
 7984        if match and not self._match(TokenType.REFERENCES):
 7985            return None
 7986
 7987        expressions: list | None = None
 7988        this = self._parse_table(schema=True)
 7989        options = self._parse_key_constraint_options()
 7990        return self.expression(exp.Reference(this=this, expressions=expressions, options=options))
 7991
 7992    def _parse_foreign_key(self) -> exp.ForeignKey:
 7993        expressions = (
 7994            self._parse_wrapped_id_vars()
 7995            if not self._match(TokenType.REFERENCES, advance=False)
 7996            else None
 7997        )
 7998        reference = self._parse_references()
 7999        on_options = {}
 8000
 8001        while self._match(TokenType.ON):
 8002            if not self._match_set((TokenType.DELETE, TokenType.UPDATE)):
 8003                self.raise_error("Expected DELETE or UPDATE")
 8004
 8005            kind = self._prev.text.lower()
 8006
 8007            if self._match_text_seq("NO", "ACTION"):
 8008                action = "NO ACTION"
 8009            elif self._match(TokenType.SET):
 8010                self._match_set((TokenType.NULL, TokenType.DEFAULT))
 8011                action = "SET " + self._prev.text.upper()
 8012            else:
 8013                self._advance()
 8014                action = self._prev.text.upper()
 8015
 8016            on_options[kind] = action
 8017
 8018        return self.expression(
 8019            exp.ForeignKey(
 8020                expressions=expressions,
 8021                reference=reference,
 8022                options=self._parse_key_constraint_options(),
 8023                **on_options,
 8024            )
 8025        )
 8026
 8027    def _parse_primary_key_part(self) -> exp.Expr | None:
 8028        return self._parse_field()
 8029
 8030    def _parse_period_for_system_time(self) -> exp.PeriodForSystemTimeConstraint | None:
 8031        if not self._match_text_seq("FOR", "SYSTEM_TIME"):
 8032            self._retreat(self._index - 1)
 8033            return None
 8034
 8035        id_vars = self._parse_wrapped_id_vars()
 8036        return self.expression(
 8037            exp.PeriodForSystemTimeConstraint(
 8038                this=seq_get(id_vars, 0), expression=seq_get(id_vars, 1)
 8039            )
 8040        )
 8041
 8042    def _parse_primary_key(
 8043        self,
 8044        wrapped_optional: bool = False,
 8045        in_props: bool = False,
 8046        named_primary_key: bool = False,
 8047    ) -> exp.PrimaryKeyColumnConstraint | exp.PrimaryKey:
 8048        desc = (
 8049            self._prev.token_type == TokenType.DESC
 8050            if self._match_set((TokenType.ASC, TokenType.DESC))
 8051            else None
 8052        )
 8053
 8054        this = None
 8055        if (
 8056            named_primary_key
 8057            and self._curr.text.upper() not in self.CONSTRAINT_PARSERS
 8058            and self._next
 8059            and self._next.token_type == TokenType.L_PAREN
 8060        ):
 8061            this = self._parse_id_var()
 8062
 8063        if not in_props and not self._match(TokenType.L_PAREN, advance=False):
 8064            return self.expression(
 8065                exp.PrimaryKeyColumnConstraint(
 8066                    desc=desc, options=self._parse_key_constraint_options()
 8067                )
 8068            )
 8069
 8070        expressions = self._parse_wrapped_csv(
 8071            self._parse_primary_key_part, optional=wrapped_optional
 8072        )
 8073
 8074        return self.expression(
 8075            exp.PrimaryKey(
 8076                this=this,
 8077                expressions=expressions,
 8078                include=self._parse_index_params(),
 8079                options=self._parse_key_constraint_options(),
 8080            )
 8081        )
 8082
 8083    def _parse_bracket_key_value(self, is_map: bool = False) -> exp.Expr | None:
 8084        return self._parse_slice(self._parse_alias(self._parse_disjunction(), explicit=True))
 8085
 8086    def _parse_odbc_datetime_literal(self) -> exp.Expr:
 8087        """
 8088        Parses a datetime column in ODBC format. We parse the column into the corresponding
 8089        types, for example `{d'yyyy-mm-dd'}` will be parsed as a `Date` column, exactly the
 8090        same as we did for `DATE('yyyy-mm-dd')`.
 8091
 8092        Reference:
 8093        https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/date-time-and-timestamp-literals
 8094        """
 8095        self._match(TokenType.VAR)
 8096        exp_class = self.ODBC_DATETIME_LITERALS[self._prev.text.lower()]
 8097        expression = self.expression(exp_class(this=self._parse_string()))
 8098        if not self._match(TokenType.R_BRACE):
 8099            self.raise_error("Expected }")
 8100        return expression
 8101
 8102    def _parse_bracket(self, this: exp.Expr | None = None) -> exp.Expr | None:
 8103        if not self._match_set(self.BRACKETS):
 8104            return this
 8105
 8106        if self.MAP_KEYS_ARE_ARBITRARY_EXPRESSIONS:
 8107            map_token = seq_get(self._tokens, self._index - 2)
 8108            parse_map = map_token is not None and map_token.text.upper() == "MAP"
 8109        else:
 8110            parse_map = False
 8111
 8112        bracket_kind = self._prev.token_type
 8113        if (
 8114            bracket_kind == TokenType.L_BRACE
 8115            and self._curr
 8116            and self._curr.token_type == TokenType.VAR
 8117            and self._curr.text.lower() in self.ODBC_DATETIME_LITERALS
 8118        ):
 8119            return self._parse_odbc_datetime_literal()
 8120
 8121        expressions = self._parse_csv(
 8122            lambda: self._parse_bracket_key_value(is_map=bracket_kind == TokenType.L_BRACE)
 8123        )
 8124
 8125        if bracket_kind == TokenType.L_BRACKET and not self._match(TokenType.R_BRACKET):
 8126            self.raise_error("Expected ]")
 8127        elif bracket_kind == TokenType.L_BRACE and not self._match(TokenType.R_BRACE):
 8128            self.raise_error("Expected }")
 8129
 8130        # https://duckdb.org/docs/sql/data_types/struct.html#creating-structs
 8131        if bracket_kind == TokenType.L_BRACE:
 8132            this = self.expression(
 8133                exp.Struct(
 8134                    expressions=self._kv_to_prop_eq(expressions=expressions, parse_map=parse_map)
 8135                )
 8136            )
 8137        elif not this:
 8138            this = build_array_constructor(
 8139                exp.Array, args=expressions, bracket_kind=bracket_kind, dialect=self.dialect
 8140            )
 8141        else:
 8142            constructor_type = self.ARRAY_CONSTRUCTORS.get(this.name.upper())
 8143            if constructor_type:
 8144                return build_array_constructor(
 8145                    constructor_type,
 8146                    args=expressions,
 8147                    bracket_kind=bracket_kind,
 8148                    dialect=self.dialect,
 8149                )
 8150
 8151            expressions = apply_index_offset(
 8152                this, expressions, -self.dialect.INDEX_OFFSET, dialect=self.dialect
 8153            )
 8154            this = self.expression(
 8155                exp.Bracket(this=this, expressions=expressions), comments=this.pop_comments()
 8156            )
 8157
 8158        self._add_comments(this)
 8159        return self._parse_bracket(this)
 8160
 8161    def _parse_slice(self, this: exp.Expr | None) -> exp.Expr | None:
 8162        if not self._match(TokenType.COLON):
 8163            return this
 8164
 8165        if self._match_pair(TokenType.DASH, TokenType.COLON, advance=False):
 8166            self._advance()
 8167            end: exp.Expr | None = -exp.Literal.number("1")
 8168        else:
 8169            end = self._parse_assignment()
 8170        step = self._parse_unary() if self._match(TokenType.COLON) else None
 8171        return self.expression(exp.Slice(this=this, expression=end, step=step))
 8172
 8173    def _parse_case(self) -> exp.Expr | None:
 8174        if self._match(TokenType.DOT, advance=False):
 8175            # Avoid raising on valid expressions like case.*, supported by, e.g., spark & snowflake
 8176            self._retreat(self._index - 1)
 8177            return None
 8178
 8179        ifs = []
 8180        default = None
 8181
 8182        comments = self._prev_comments
 8183        expression = self._parse_disjunction()
 8184
 8185        while self._match(TokenType.WHEN):
 8186            this = self._parse_disjunction()
 8187            self._match(TokenType.THEN)
 8188            then = self._parse_disjunction()
 8189            ifs.append(self.expression(exp.If(this=this, true=then)))
 8190
 8191        if self._match(TokenType.ELSE):
 8192            default = self._parse_disjunction()
 8193
 8194        if not self._match(TokenType.END):
 8195            if isinstance(default, exp.Interval) and default.this.sql().upper() == "END":
 8196                default = exp.column("interval")
 8197            else:
 8198                self.raise_error("Expected END after CASE", self._prev)
 8199
 8200        return self.expression(
 8201            exp.Case(this=expression, ifs=ifs, default=default), comments=comments
 8202        )
 8203
 8204    def _parse_if(self) -> exp.Expr | None:
 8205        if self._match(TokenType.L_PAREN):
 8206            args = self._parse_csv(
 8207                lambda: self._parse_alias(self._parse_assignment(), explicit=True)
 8208            )
 8209            this = self.validate_expression(exp.If.from_arg_list(args), args)
 8210            self._match_r_paren()
 8211        else:
 8212            index = self._index - 1
 8213
 8214            if self.NO_PAREN_IF_COMMANDS and index == 0:
 8215                return self._parse_as_command(self._prev)
 8216
 8217            condition = self._parse_disjunction()
 8218
 8219            if not condition or not self._match(TokenType.THEN):
 8220                self._retreat(index)
 8221                return None
 8222
 8223            true = self._parse_disjunction()
 8224            false = self._parse_disjunction() if self._match(TokenType.ELSE) else None
 8225            self._match(TokenType.END)
 8226            this = self.expression(exp.If(this=condition, true=true, false=false))
 8227
 8228        return this
 8229
 8230    def _parse_next_value_for(self) -> exp.Expr | None:
 8231        if not self._match_text_seq("VALUE", "FOR"):
 8232            self._retreat(self._index - 1)
 8233            return None
 8234
 8235        return self.expression(
 8236            exp.NextValueFor(
 8237                this=self._parse_column(),
 8238                order=self._match(TokenType.OVER) and self._parse_wrapped(self._parse_order),
 8239            )
 8240        )
 8241
 8242    def _parse_extract(self) -> exp.Extract | exp.Anonymous:
 8243        this = self._parse_function() or self._parse_var_or_string(upper=True)
 8244
 8245        if self._match(TokenType.FROM):
 8246            return self.expression(exp.Extract(this=this, expression=self._parse_bitwise()))
 8247
 8248        if not self._match(TokenType.COMMA):
 8249            self.raise_error("Expected FROM or comma after EXTRACT", self._prev)
 8250
 8251        return self.expression(exp.Extract(this=this, expression=self._parse_bitwise()))
 8252
 8253    def _parse_gap_fill(self) -> exp.GapFill:
 8254        self._match(TokenType.TABLE)
 8255        this = self._parse_table()
 8256
 8257        self._match(TokenType.COMMA)
 8258        args = [this, *self._parse_csv(self._parse_lambda)]
 8259
 8260        gap_fill = exp.GapFill.from_arg_list(args)
 8261        return self.validate_expression(gap_fill, args)
 8262
 8263    def _parse_char(self) -> exp.Chr:
 8264        return self.expression(
 8265            exp.Chr(
 8266                expressions=self._parse_csv(self._parse_assignment),
 8267                charset=self._match(TokenType.USING) and self._parse_charset_name(),
 8268            )
 8269        )
 8270
 8271    def _parse_charset_name(self) -> exp.Expr | None:
 8272        """
 8273        Parse a charset name after USING or CHARACTER SET. Dialects that need to preserve quoting
 8274        for specific name shapes override this.
 8275        """
 8276        return self._parse_var(
 8277            tokens={TokenType.BINARY, TokenType.IDENTIFIER},
 8278        )
 8279
 8280    def _parse_weight_string(self) -> exp.WeightString | exp.Anonymous:
 8281        name = self._tokens[self._index - 2].text
 8282        args = self._parse_csv(self._parse_assignment)
 8283
 8284        # MySQL also accepts an undocumented WEIGHT_STRING(str, n, n, n) form
 8285        if len(args) != 1:
 8286            return self.expression(exp.Anonymous(this=name, expressions=args))
 8287
 8288        return self.expression(
 8289            exp.WeightString(this=args[0], to=self._match(TokenType.ALIAS) and self._parse_types())
 8290        )
 8291
 8292    def _parse_cast(self, strict: bool, safe: bool | None = None) -> exp.Expr:
 8293        this = self._parse_assignment()
 8294
 8295        if not self._match(TokenType.ALIAS):
 8296            if self._match(TokenType.COMMA):
 8297                return self.expression(exp.CastToStrType(this=this, to=self._parse_string()))
 8298
 8299            self.raise_error("Expected AS after CAST")
 8300
 8301        fmt = None
 8302        to = self._parse_types(with_collation=True)
 8303
 8304        default = None
 8305        if self._match(TokenType.DEFAULT):
 8306            default = self._parse_bitwise()
 8307            self._match_text_seq("ON", "CONVERSION", "ERROR")
 8308
 8309        if self._match_set((TokenType.FORMAT, TokenType.COMMA)):
 8310            fmt_string = self._parse_wrapped(self._parse_string, optional=True)
 8311            fmt = self._parse_at_time_zone(fmt_string)
 8312
 8313            if not to:
 8314                to = exp.DType.UNKNOWN.into_expr()
 8315            if to.this in exp.DataType.TEMPORAL_TYPES:
 8316                this = self.expression(
 8317                    (exp.StrToDate if to.this == exp.DType.DATE else exp.StrToTime)(
 8318                        this=this,
 8319                        format=exp.Literal.string(
 8320                            format_time(
 8321                                fmt_string.this if fmt_string else "",
 8322                                self.dialect.FORMAT_MAPPING or self.dialect.TIME_MAPPING,
 8323                                self.dialect.FORMAT_TRIE or self.dialect.TIME_TRIE,
 8324                            )
 8325                        ),
 8326                        safe=safe,
 8327                    )
 8328                )
 8329
 8330                if isinstance(fmt, exp.AtTimeZone) and isinstance(this, exp.StrToTime):
 8331                    this.set("zone", fmt.args["zone"])
 8332                return this
 8333        elif not to:
 8334            self.raise_error("Expected TYPE after CAST")
 8335        elif isinstance(to, exp.Identifier):
 8336            to = exp.DataType.from_str(to.name, dialect=self.dialect, udt=True)
 8337        else:
 8338            to = self._parse_char_charset(to)
 8339
 8340        return self.build_cast(
 8341            strict=strict,
 8342            this=this,
 8343            to=to,
 8344            format=fmt,
 8345            safe=safe,
 8346            action=self._parse_var_from_options(self.CAST_ACTIONS, raise_unmatched=False),
 8347            default=default,
 8348        )
 8349
 8350    def _parse_string_agg(self) -> exp.GroupConcat:
 8351        if self._match(TokenType.DISTINCT):
 8352            args: list[exp.Expr | None] = [
 8353                self.expression(exp.Distinct(expressions=[self._parse_disjunction()]))
 8354            ]
 8355            if self._match(TokenType.COMMA):
 8356                args.extend(self._parse_csv(self._parse_disjunction))
 8357        else:
 8358            args = self._parse_csv(self._parse_disjunction)  # type: ignore
 8359
 8360        if self._match_text_seq("ON", "OVERFLOW"):
 8361            # trino: LISTAGG(expression [, separator] [ON OVERFLOW overflow_behavior])
 8362            if self._match_text_seq("ERROR"):
 8363                on_overflow: exp.Expr | None = exp.var("ERROR")
 8364            else:
 8365                self._match_text_seq("TRUNCATE")
 8366                on_overflow = self.expression(
 8367                    exp.OverflowTruncateBehavior(
 8368                        this=self._parse_string(),
 8369                        with_count=(
 8370                            self._match_text_seq("WITH", "COUNT")
 8371                            or not self._match_text_seq("WITHOUT", "COUNT")
 8372                        ),
 8373                    )
 8374                )
 8375        else:
 8376            on_overflow = None
 8377
 8378        index = self._index
 8379        if not self._match(TokenType.R_PAREN) and args:
 8380            # postgres: STRING_AGG([DISTINCT] expression, separator [ORDER BY expression1 {ASC | DESC} [, ...]])
 8381            # bigquery: STRING_AGG([DISTINCT] expression [, separator] [ORDER BY key [{ASC | DESC}] [, ... ]] [LIMIT n])
 8382            # The order is parsed through `this` as a canonicalization for WITHIN GROUPs
 8383            args[0] = self._parse_limit(this=self._parse_order(this=args[0]))
 8384            return self.expression(exp.GroupConcat(this=args[0], separator=seq_get(args, 1)))
 8385
 8386        # Checks if we can parse an order clause: WITHIN GROUP (ORDER BY <order_by_expression_list> [ASC | DESC]).
 8387        # This is done "manually", instead of letting _parse_window parse it into an exp.WithinGroup node, so that
 8388        # the STRING_AGG call is parsed like in MySQL / SQLite and can thus be transpiled more easily to them.
 8389        if not self._match_text_seq("WITHIN", "GROUP"):
 8390            self._retreat(index)
 8391            return self.validate_expression(exp.GroupConcat.from_arg_list(args), args)
 8392
 8393        # The corresponding match_r_paren will be called in parse_function (caller)
 8394        self._match_l_paren()
 8395
 8396        return self.expression(
 8397            exp.GroupConcat(
 8398                this=self._parse_order(this=seq_get(args, 0)),
 8399                separator=seq_get(args, 1),
 8400                on_overflow=on_overflow,
 8401            )
 8402        )
 8403
 8404    def _parse_convert(self, strict: bool, safe: bool | None = None) -> exp.Expr | None:
 8405        this = self._parse_bitwise()
 8406
 8407        if self._match(TokenType.USING):
 8408            to: exp.Expr | None = exp.DType.CHARACTER_SET.into_expr(kind=self._parse_charset_name())
 8409        elif self._match(TokenType.COMMA):
 8410            to = self._parse_char_charset(self._parse_types())
 8411        else:
 8412            to = None
 8413
 8414        return self.build_cast(strict=strict, this=this, to=to, safe=safe)
 8415
 8416    def _parse_char_charset(self, to: exp.Expr | None) -> exp.Expr | None:
 8417        # MySQL: CHAR[(N)] CHARACTER SET charset_name, the length is kept
 8418        if (
 8419            isinstance(to, exp.DataType)
 8420            and to.this == exp.DType.CHAR
 8421            and (self._match(TokenType.CHARACTER_SET) or self._match_text_seq("CHARACTER", "SET"))
 8422        ):
 8423            to.set("this", exp.DType.CHARACTER_SET)
 8424            to.set("kind", self._parse_var_or_string())
 8425        return to
 8426
 8427    def _parse_xml_element(self) -> exp.XMLElement:
 8428        if self._match_text_seq("EVALNAME"):
 8429            evalname = True
 8430            this = self._parse_bitwise()
 8431        else:
 8432            evalname = None
 8433            self._match_text_seq("NAME")
 8434            this = self._parse_id_var()
 8435
 8436        return self.expression(
 8437            exp.XMLElement(
 8438                this=this,
 8439                expressions=self._match(TokenType.COMMA) and self._parse_csv(self._parse_bitwise),
 8440                evalname=evalname,
 8441            )
 8442        )
 8443
 8444    def _parse_xml_table(self) -> exp.XMLTable:
 8445        namespaces = None
 8446        passing = None
 8447        columns = None
 8448
 8449        if self._match_text_seq("XMLNAMESPACES", "("):
 8450            namespaces = self._parse_xml_namespace()
 8451            self._match_text_seq(")", ",")
 8452
 8453        this = self._parse_string()
 8454
 8455        if self._match_text_seq("PASSING"):
 8456            # The BY VALUE keywords are optional and are provided for semantic clarity
 8457            self._match_text_seq("BY", "VALUE")
 8458            passing = self._parse_csv(self._parse_column)
 8459
 8460        by_ref = self._match_text_seq("RETURNING", "SEQUENCE", "BY", "REF")
 8461
 8462        if self._match_text_seq("COLUMNS"):
 8463            columns = self._parse_csv(self._parse_field_def)
 8464
 8465        return self.expression(
 8466            exp.XMLTable(
 8467                this=this, namespaces=namespaces, passing=passing, columns=columns, by_ref=by_ref
 8468            )
 8469        )
 8470
 8471    def _parse_xml_namespace(self) -> list[exp.XMLNamespace]:
 8472        namespaces = []
 8473
 8474        while True:
 8475            if self._match(TokenType.DEFAULT):
 8476                uri = self._parse_string()
 8477            else:
 8478                uri = self._parse_alias(self._parse_string())
 8479            namespaces.append(self.expression(exp.XMLNamespace(this=uri)))
 8480            if not self._match(TokenType.COMMA):
 8481                break
 8482
 8483        return namespaces
 8484
 8485    def _parse_decode(self) -> exp.Decode | exp.DecodeCase | None:
 8486        args = self._parse_csv(self._parse_disjunction)
 8487
 8488        if len(args) < 3:
 8489            return self.expression(exp.Decode(this=seq_get(args, 0), charset=seq_get(args, 1)))
 8490
 8491        return self.expression(exp.DecodeCase(expressions=args))
 8492
 8493    def _parse_json_key_value(self) -> exp.JSONKeyValue | None:
 8494        self._match_text_seq("KEY")
 8495        key = self._parse_column()
 8496        self._match_set(self.JSON_KEY_VALUE_SEPARATOR_TOKENS)
 8497        self._match_text_seq("VALUE")
 8498        value = self._parse_bitwise()
 8499
 8500        if not key and not value:
 8501            return None
 8502        return self.expression(exp.JSONKeyValue(this=key, expression=value))
 8503
 8504    def _parse_format_json(self, this: exp.Expr | None) -> exp.Expr | None:
 8505        if not this or not self._match_text_seq("FORMAT", "JSON"):
 8506            return this
 8507
 8508        return self.expression(exp.FormatJson(this=this))
 8509
 8510    def _parse_on_condition(self) -> exp.OnCondition | None:
 8511        # MySQL uses "X ON EMPTY Y ON ERROR" (e.g. JSON_VALUE) while Oracle uses the opposite (e.g. JSON_EXISTS)
 8512        if self.dialect.ON_CONDITION_EMPTY_BEFORE_ERROR:
 8513            empty = self._parse_on_handling("EMPTY", *self.ON_CONDITION_TOKENS)
 8514            error = self._parse_on_handling("ERROR", *self.ON_CONDITION_TOKENS)
 8515        else:
 8516            error = self._parse_on_handling("ERROR", *self.ON_CONDITION_TOKENS)
 8517            empty = self._parse_on_handling("EMPTY", *self.ON_CONDITION_TOKENS)
 8518
 8519        null = self._parse_on_handling("NULL", *self.ON_CONDITION_TOKENS)
 8520
 8521        if not empty and not error and not null:
 8522            return None
 8523
 8524        return self.expression(exp.OnCondition(empty=empty, error=error, null=null))
 8525
 8526    def _parse_on_handling(self, on: str, *values: str) -> str | None | exp.Expr | None:
 8527        # Parses the "X ON Y" or "DEFAULT <expr> ON Y syntax, e.g. NULL ON NULL (Oracle, T-SQL, MySQL)
 8528        for value in values:
 8529            if self._match_text_seq(value, "ON", on):
 8530                return f"{value} ON {on}"
 8531
 8532        index = self._index
 8533        if self._match(TokenType.DEFAULT):
 8534            default_value = self._parse_bitwise()
 8535            if self._match_text_seq("ON", on):
 8536                return default_value
 8537
 8538            self._retreat(index)
 8539
 8540        return None
 8541
 8542    @t.overload
 8543    def _parse_json_object(self, agg: t.Literal[False]) -> exp.JSONObject: ...
 8544
 8545    @t.overload
 8546    def _parse_json_object(self, agg: t.Literal[True]) -> exp.JSONObjectAgg: ...
 8547
 8548    def _parse_json_object(self, agg=False):
 8549        star = self._parse_star()
 8550        expressions = (
 8551            [star]
 8552            if star
 8553            else self._parse_csv(lambda: self._parse_format_json(self._parse_json_key_value()))
 8554        )
 8555        null_handling = self._parse_on_handling("NULL", "NULL", "ABSENT")
 8556
 8557        unique_keys = None
 8558        if self._match_text_seq("WITH", "UNIQUE"):
 8559            unique_keys = True
 8560        elif self._match_text_seq("WITHOUT", "UNIQUE"):
 8561            unique_keys = False
 8562
 8563        self._match_text_seq("KEYS")
 8564
 8565        return_type = self._match_text_seq("RETURNING") and self._parse_format_json(
 8566            self._parse_type()
 8567        )
 8568        encoding = self._match_text_seq("ENCODING") and self._parse_var()
 8569
 8570        return self.expression(
 8571            (exp.JSONObjectAgg if agg else exp.JSONObject)(
 8572                expressions=expressions,
 8573                null_handling=null_handling,
 8574                unique_keys=unique_keys,
 8575                return_type=return_type,
 8576                encoding=encoding,
 8577            )
 8578        )
 8579
 8580    # Note: this is currently incomplete; it only implements the "JSON_value_column" part
 8581    def _parse_json_column_def(self) -> exp.JSONColumnDef:
 8582        if not self._match_text_seq("NESTED"):
 8583            this = self._parse_id_var()
 8584            ordinality = self._match_pair(TokenType.FOR, TokenType.ORDINALITY)
 8585            kind = self._parse_types(allow_identifiers=False)
 8586            nested = None
 8587        else:
 8588            this = None
 8589            ordinality = None
 8590            kind = None
 8591            nested = True
 8592
 8593        format_json = self._match_text_seq("FORMAT", "JSON")
 8594        path = self._match_text_seq("PATH") and self._parse_string()
 8595        nested_schema = nested and self._parse_json_schema()
 8596
 8597        return self.expression(
 8598            exp.JSONColumnDef(
 8599                this=this,
 8600                kind=kind,
 8601                path=path,
 8602                nested_schema=nested_schema,
 8603                ordinality=ordinality,
 8604                format_json=format_json,
 8605            )
 8606        )
 8607
 8608    def _parse_json_schema(self) -> exp.JSONSchema:
 8609        self._match_text_seq("COLUMNS")
 8610        return self.expression(
 8611            exp.JSONSchema(
 8612                expressions=self._parse_wrapped_csv(self._parse_json_column_def, optional=True)
 8613            )
 8614        )
 8615
 8616    def _parse_json_table(self) -> exp.JSONTable:
 8617        this = self._parse_format_json(self._parse_bitwise())
 8618        path = self._match(TokenType.COMMA) and self._parse_string()
 8619        error_handling = self._parse_on_handling("ERROR", "ERROR", "NULL")
 8620        empty_handling = self._parse_on_handling("EMPTY", "ERROR", "NULL")
 8621        schema = self._parse_json_schema()
 8622
 8623        return exp.JSONTable(
 8624            this=this,
 8625            schema=schema,
 8626            path=path,
 8627            error_handling=error_handling,
 8628            empty_handling=empty_handling,
 8629        )
 8630
 8631    def _parse_match_against(self) -> exp.MatchAgainst:
 8632        if self._match_text_seq("TABLE"):
 8633            # parse SingleStore MATCH(TABLE ...) syntax
 8634            # https://docs.singlestore.com/cloud/reference/sql-reference/full-text-search-functions/match/
 8635            expressions = []
 8636            table = self._parse_table()
 8637            if table:
 8638                expressions = [table]
 8639        else:
 8640            expressions = self._parse_csv(self._parse_column)
 8641
 8642        self._match_text_seq(")", "AGAINST", "(")
 8643
 8644        this = self._parse_string()
 8645
 8646        if self._match_text_seq("IN", "NATURAL", "LANGUAGE", "MODE"):
 8647            modifier = "IN NATURAL LANGUAGE MODE"
 8648            if self._match_text_seq("WITH", "QUERY", "EXPANSION"):
 8649                modifier = f"{modifier} WITH QUERY EXPANSION"
 8650        elif self._match_text_seq("IN", "BOOLEAN", "MODE"):
 8651            modifier = "IN BOOLEAN MODE"
 8652        elif self._match_text_seq("WITH", "QUERY", "EXPANSION"):
 8653            modifier = "WITH QUERY EXPANSION"
 8654        else:
 8655            modifier = None
 8656
 8657        return self.expression(
 8658            exp.MatchAgainst(this=this, expressions=expressions, modifier=modifier)
 8659        )
 8660
 8661    # https://learn.microsoft.com/en-us/sql/t-sql/functions/openjson-transact-sql?view=sql-server-ver16
 8662    def _parse_open_json(self) -> exp.OpenJSON:
 8663        this = self._parse_bitwise()
 8664        path = self._match(TokenType.COMMA) and self._parse_string()
 8665
 8666        def _parse_open_json_column_def() -> exp.OpenJSONColumnDef:
 8667            this = self._parse_field(any_token=True)
 8668            kind = self._parse_types()
 8669            path = self._parse_string()
 8670            as_json = self._match_pair(TokenType.ALIAS, TokenType.JSON)
 8671
 8672            return self.expression(
 8673                exp.OpenJSONColumnDef(this=this, kind=kind, path=path, as_json=as_json)
 8674            )
 8675
 8676        expressions = None
 8677        if self._match_pair(TokenType.R_PAREN, TokenType.WITH):
 8678            self._match_l_paren()
 8679            expressions = self._parse_csv(_parse_open_json_column_def)
 8680
 8681        return self.expression(exp.OpenJSON(this=this, path=path, expressions=expressions))
 8682
 8683    def _parse_position(self, haystack_first: bool = False) -> exp.StrPosition:
 8684        args = self._parse_csv(self._parse_bitwise)
 8685
 8686        if self._match(TokenType.IN):
 8687            return self.expression(
 8688                exp.StrPosition(this=self._parse_bitwise(), substr=seq_get(args, 0))
 8689            )
 8690
 8691        if haystack_first:
 8692            haystack = seq_get(args, 0)
 8693            needle = seq_get(args, 1)
 8694        else:
 8695            haystack = seq_get(args, 1)
 8696            needle = seq_get(args, 0)
 8697
 8698        return self.expression(
 8699            exp.StrPosition(this=haystack, substr=needle, position=seq_get(args, 2))
 8700        )
 8701
 8702    def _parse_join_hint(self, func_name: str) -> exp.JoinHint:
 8703        args = self._parse_csv(self._parse_table)
 8704        return exp.JoinHint(this=func_name.upper(), expressions=args)
 8705
 8706    def _parse_substring(self) -> exp.Substring:
 8707        # Postgres supports the form: substring(string [from int] [for int])
 8708        # (despite being undocumented, the reverse order also works)
 8709        # https://www.postgresql.org/docs/9.1/functions-string.html @ Table 9-6
 8710
 8711        args = t.cast(list[t.Optional[exp.Expr]], self._parse_csv(self._parse_bitwise))
 8712
 8713        start, length = None, None
 8714
 8715        while self._curr:
 8716            if self._match(TokenType.FROM):
 8717                start = self._parse_bitwise()
 8718            elif self._match(TokenType.FOR):
 8719                if not start:
 8720                    start = exp.Literal.number(1)
 8721                length = self._parse_bitwise()
 8722            else:
 8723                break
 8724
 8725        if start:
 8726            args.append(start)
 8727        if length:
 8728            args.append(length)
 8729
 8730        return self.validate_expression(exp.Substring.from_arg_list(args), args)
 8731
 8732    def _parse_trim(self) -> exp.Trim:
 8733        # https://www.w3resource.com/sql/character-functions/trim.php
 8734        # https://docs.oracle.com/javadb/10.8.3.0/ref/rreftrimfunc.html
 8735
 8736        position = None
 8737        collation = None
 8738        expression = None
 8739
 8740        if self._match_texts(self.TRIM_TYPES):
 8741            position = self._prev.text.upper()
 8742
 8743        this = self._parse_bitwise()
 8744        if self._match_set((TokenType.FROM, TokenType.COMMA)):
 8745            invert_order = self._prev.token_type == TokenType.FROM or self.TRIM_PATTERN_FIRST
 8746            expression = self._parse_bitwise()
 8747
 8748            if invert_order:
 8749                this, expression = expression, this
 8750
 8751        if self._match(TokenType.COLLATE):
 8752            collation = self._parse_bitwise()
 8753
 8754        return self.expression(
 8755            exp.Trim(this=this, position=position, expression=expression, collation=collation)
 8756        )
 8757
 8758    def _parse_window_clause(self) -> list[exp.Expr] | None:
 8759        return self._parse_csv(self._parse_named_window) if self._match(TokenType.WINDOW) else None
 8760
 8761    def _parse_named_window(self) -> exp.Expr | None:
 8762        return self._parse_window(self._parse_id_var(), alias=True)
 8763
 8764    def _parse_respect_or_ignore_nulls(self, this: exp.Expr | None) -> exp.Expr | None:
 8765        if self._curr.token_type == TokenType.VAR:
 8766            if self._match_text_seq("IGNORE", "NULLS"):
 8767                return self.expression(exp.IgnoreNulls(this=this))
 8768            if self._match_text_seq("RESPECT", "NULLS"):
 8769                return self.expression(exp.RespectNulls(this=this))
 8770        return this
 8771
 8772    def _parse_having_max(self, this: exp.Expr | None) -> exp.Expr | None:
 8773        if self._match(TokenType.HAVING):
 8774            self._match_texts(("MAX", "MIN"))
 8775            max = self._prev.text.upper() != "MIN"
 8776            return self.expression(
 8777                exp.HavingMax(this=this, expression=self._parse_column(), max=max)
 8778            )
 8779
 8780        return this
 8781
 8782    def _parse_window(self, this: exp.Expr | None, alias: bool = False) -> exp.Expr | None:
 8783        func = this
 8784        comments = func.comments if isinstance(func, exp.Expr) else None
 8785
 8786        # https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/img_text/nth_value.html
 8787        if self.SUPPORTS_NTH_VALUE_FROM_MODIFIER and isinstance(this, exp.NthValue):
 8788            if self._match_text_seq("FROM", "FIRST"):
 8789                this.set("from_first", True)
 8790            elif self._match_text_seq("FROM", "LAST"):
 8791                this.set("from_first", False)
 8792
 8793        # T-SQL allows the OVER (...) syntax after WITHIN GROUP.
 8794        # https://learn.microsoft.com/en-us/sql/t-sql/functions/percentile-disc-transact-sql?view=sql-server-ver16
 8795        if self._match_text_seq("WITHIN", "GROUP"):
 8796            order = self._parse_wrapped(self._parse_order)
 8797            this = self.expression(exp.WithinGroup(this=this, expression=order))
 8798
 8799        if self._match_pair(TokenType.FILTER, TokenType.L_PAREN):
 8800            self._match(TokenType.WHERE)
 8801            this = self.expression(
 8802                exp.Filter(this=this, expression=self._parse_where(skip_where_token=True))
 8803            )
 8804            self._match_r_paren()
 8805
 8806        # SQL spec defines an optional [ { IGNORE | RESPECT } NULLS ] OVER
 8807        # Some dialects choose to implement and some do not.
 8808        # https://dev.mysql.com/doc/refman/8.0/en/window-function-descriptions.html
 8809
 8810        # There is some code above in _parse_lambda that handles
 8811        #   SELECT FIRST_VALUE(TABLE.COLUMN IGNORE|RESPECT NULLS) OVER ...
 8812
 8813        # The below changes handle
 8814        #   SELECT FIRST_VALUE(TABLE.COLUMN) IGNORE|RESPECT NULLS OVER ...
 8815
 8816        # Oracle allows both formats
 8817        #   (https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/img_text/first_value.html)
 8818        #   and Snowflake chose to do the same for familiarity
 8819        #   https://docs.snowflake.com/en/sql-reference/functions/first_value.html#usage-notes
 8820        if isinstance(this, exp.AggFunc):
 8821            ignore_respect = find_in_scope(this, exp.IgnoreNulls, exp.RespectNulls)
 8822
 8823            if ignore_respect and ignore_respect is not this:
 8824                ignore_respect.replace(ignore_respect.this)
 8825                this = self.expression(ignore_respect.__class__(this=this))
 8826
 8827        this = self._parse_respect_or_ignore_nulls(this)
 8828
 8829        # bigquery select from window x AS (partition by ...)
 8830        if alias:
 8831            over = None
 8832            self._match(TokenType.ALIAS)
 8833        elif not self._match_set(self.WINDOW_BEFORE_PAREN_TOKENS):
 8834            return this
 8835        else:
 8836            over = self._prev.text.upper()
 8837
 8838        if comments and isinstance(func, exp.Expr):
 8839            func.pop_comments()
 8840
 8841        if not self._match(TokenType.L_PAREN):
 8842            return self.expression(
 8843                exp.Window(this=this, alias=self._parse_id_var(False), over=over), comments=comments
 8844            )
 8845
 8846        window_alias = self._parse_id_var(any_token=False, tokens=self.WINDOW_ALIAS_TOKENS)
 8847
 8848        first: bool | None = True if self._match(TokenType.FIRST) else None
 8849        if self._match_text_seq("LAST"):
 8850            first = False
 8851
 8852        partition, order = self._parse_partition_and_order()
 8853        kind = (
 8854            self._match_set((TokenType.ROWS, TokenType.RANGE)) or self._match_text_seq("GROUPS")
 8855        ) and self._prev.text
 8856
 8857        if kind:
 8858            self._match(TokenType.BETWEEN)
 8859            start = self._parse_window_spec()
 8860
 8861            end = self._parse_window_spec() if self._match(TokenType.AND) else {}
 8862            exclude = (
 8863                self._parse_var_from_options(self.WINDOW_EXCLUDE_OPTIONS)
 8864                if self._match_text_seq("EXCLUDE")
 8865                else None
 8866            )
 8867
 8868            spec = self.expression(
 8869                exp.WindowSpec(
 8870                    kind=kind,
 8871                    start=start["value"],
 8872                    start_side=start["side"],
 8873                    end=end.get("value"),
 8874                    end_side=end.get("side"),
 8875                    exclude=exclude,
 8876                )
 8877            )
 8878        else:
 8879            spec = None
 8880
 8881        self._match_r_paren()
 8882
 8883        window = self.expression(
 8884            exp.Window(
 8885                this=this,
 8886                partition_by=partition,
 8887                order=order,
 8888                spec=spec,
 8889                alias=window_alias,
 8890                over=over,
 8891                first=first,
 8892            ),
 8893            comments=comments,
 8894        )
 8895
 8896        # This covers Oracle's FIRST/LAST syntax: aggregate KEEP (...) OVER (...)
 8897        if self._match_set(self.WINDOW_BEFORE_PAREN_TOKENS, advance=False):
 8898            return self._parse_window(window, alias=alias)
 8899
 8900        return window
 8901
 8902    def _parse_partition_and_order(
 8903        self,
 8904    ) -> tuple[list[exp.Expr], exp.Expr | None]:
 8905        return self._parse_partition_by(), self._parse_order()
 8906
 8907    def _parse_window_spec(self) -> dict[str, str | exp.Expr | None]:
 8908        self._match(TokenType.BETWEEN)
 8909
 8910        return {
 8911            "value": (
 8912                (self._match_text_seq("UNBOUNDED") and "UNBOUNDED")
 8913                or (self._match_text_seq("CURRENT", "ROW") and "CURRENT ROW")
 8914                or self._parse_bitwise()
 8915            ),
 8916            "side": self._prev.text if self._match_texts(self.WINDOW_SIDES) else None,
 8917        }
 8918
 8919    def _parse_alias(self, this: exp.Expr | None, explicit: bool = False) -> exp.Expr | None:
 8920        # In some dialects, LIMIT and OFFSET can act as both identifiers and keywords (clauses)
 8921        # so this section tries to parse the clause version and if it fails, it treats the token
 8922        # as an identifier (alias)
 8923        if self._can_parse_limit_or_offset():
 8924            return this
 8925
 8926        # WINDOW is in ID_VAR_TOKENS, so it can be consumed as an implicit alias. Detect the
 8927        # named-window clause shape (`WINDOW <ident> AS (...)`) and avoid swallowing it.
 8928        if self._can_parse_named_window():
 8929            return this
 8930
 8931        any_token = self._match(TokenType.ALIAS)
 8932        comments = self._prev_comments
 8933
 8934        if explicit and not any_token:
 8935            return this
 8936
 8937        if self._match(TokenType.L_PAREN):
 8938            aliases = self.expression(
 8939                exp.Aliases(
 8940                    this=this, expressions=self._parse_csv(lambda: self._parse_id_var(any_token))
 8941                ),
 8942                comments=comments,
 8943            )
 8944            self._match_r_paren(aliases)
 8945            return aliases
 8946
 8947        alias = self._parse_id_var(any_token, tokens=self.ALIAS_TOKENS) or (
 8948            self.STRING_ALIASES and self._parse_string_as_identifier()
 8949        )
 8950
 8951        if alias:
 8952            comments.extend(alias.pop_comments())
 8953            this = self.expression(exp.Alias(this=this, alias=alias), comments=comments)
 8954            column = this.this
 8955
 8956            # Moves the comment next to the alias in `expr /* comment */ AS alias`
 8957            if not this.comments and column and column.comments:
 8958                this.comments = column.pop_comments()
 8959
 8960        return this
 8961
 8962    def _parse_id_var(
 8963        self,
 8964        any_token: bool = True,
 8965        tokens: t.Collection[TokenType] | None = None,
 8966    ) -> exp.Expr | None:
 8967        expression = self._parse_identifier()
 8968        if not expression and (
 8969            (any_token and self._advance_any()) or self._match_set(tokens or self.ID_VAR_TOKENS)
 8970        ):
 8971            quoted = self._prev.token_type == TokenType.STRING
 8972            expression = self._identifier_expression(quoted=quoted)
 8973
 8974        return expression
 8975
 8976    def _parse_string(self) -> exp.Expr | None:
 8977        if self._match_set(self.STRING_PARSERS):
 8978            return self.STRING_PARSERS[self._prev.token_type](self, self._prev)
 8979        return self._parse_placeholder()
 8980
 8981    def _parse_string_as_identifier(self) -> exp.Identifier | None:
 8982        if not self._match(TokenType.STRING):
 8983            return None
 8984        output = exp.to_identifier(self._prev.text, quoted=True)
 8985        output.update_positions(self._prev)
 8986        return output
 8987
 8988    def _parse_number(self) -> exp.Expr | None:
 8989        if self._match_set(self.NUMERIC_PARSERS):
 8990            return self.NUMERIC_PARSERS[self._prev.token_type](self, self._prev)
 8991        return self._parse_placeholder()
 8992
 8993    def _parse_identifier(self) -> exp.Expr | None:
 8994        if self._match(TokenType.IDENTIFIER):
 8995            return self._identifier_expression(quoted=True)
 8996        return self._parse_placeholder()
 8997
 8998    def _parse_var(
 8999        self,
 9000        any_token: bool = False,
 9001        tokens: t.Collection[TokenType] | None = None,
 9002        upper: bool = False,
 9003    ) -> exp.Expr | None:
 9004        if (
 9005            (any_token and self._advance_any())
 9006            or self._match(TokenType.VAR)
 9007            or (self._match_set(tokens) if tokens else False)
 9008        ):
 9009            return self.expression(
 9010                exp.Var(this=self._prev.text.upper() if upper else self._prev.text)
 9011            )
 9012        return self._parse_placeholder()
 9013
 9014    def _advance_any(self, ignore_reserved: bool = False) -> Token | None:
 9015        if self._curr and (ignore_reserved or self._curr.token_type not in self.RESERVED_TOKENS):
 9016            self._advance()
 9017            return self._prev
 9018        return None
 9019
 9020    def _parse_var_or_string(self, upper: bool = False) -> exp.Expr | None:
 9021        return self._parse_string() or self._parse_var(any_token=True, upper=upper)
 9022
 9023    def _parse_primary_or_var(self) -> exp.Expr | None:
 9024        return self._parse_primary() or self._parse_var(any_token=True)
 9025
 9026    def _parse_null(self) -> exp.Expr | None:
 9027        if self._match_set((TokenType.NULL, TokenType.UNKNOWN)):
 9028            return self.PRIMARY_PARSERS[TokenType.NULL](self, self._prev)
 9029        return self._parse_placeholder()
 9030
 9031    def _parse_boolean(self) -> exp.Expr | None:
 9032        if self._match(TokenType.TRUE):
 9033            return self.PRIMARY_PARSERS[TokenType.TRUE](self, self._prev)
 9034        if self._match(TokenType.FALSE):
 9035            return self.PRIMARY_PARSERS[TokenType.FALSE](self, self._prev)
 9036        return self._parse_placeholder()
 9037
 9038    def _parse_star(self) -> exp.Expr | None:
 9039        if self._match(TokenType.STAR):
 9040            return self.PRIMARY_PARSERS[TokenType.STAR](self, self._prev)
 9041        return self._parse_placeholder()
 9042
 9043    def _parse_parameter(self) -> exp.Parameter:
 9044        this = self._parse_identifier() or self._parse_primary_or_var()
 9045        return self.expression(exp.Parameter(this=this))
 9046
 9047    def _parse_placeholder(self) -> exp.Expr | None:
 9048        if self._match_set(self.PLACEHOLDER_PARSERS):
 9049            placeholder = self.PLACEHOLDER_PARSERS[self._prev.token_type](self)
 9050            if placeholder:
 9051                return placeholder
 9052            self._advance(-1)
 9053        return None
 9054
 9055    def _parse_star_op(self, *keywords: str) -> list[exp.Expr] | None:
 9056        if not self._match_texts(keywords):
 9057            return None
 9058        if self._match(TokenType.L_PAREN, advance=False):
 9059            return self._parse_wrapped_csv(self._parse_expression)
 9060
 9061        expression = self._parse_alias(self._parse_disjunction(), explicit=True)
 9062        return [expression] if expression else None
 9063
 9064    def _parse_csv(
 9065        self, parse_method: t.Callable[[], T | None], sep: TokenType = TokenType.COMMA
 9066    ) -> list[T]:
 9067        parse_result = parse_method()
 9068        items = [parse_result] if parse_result is not None else []
 9069
 9070        while self._match(sep):
 9071            if isinstance(parse_result, exp.Expr):
 9072                self._add_comments(parse_result)
 9073            parse_result = parse_method()
 9074            if parse_result is not None:
 9075                items.append(parse_result)
 9076
 9077        return items
 9078
 9079    def _parse_wrapped_id_vars(self, optional: bool = False) -> list[exp.Expr]:
 9080        return self._parse_wrapped_csv(self._parse_id_var, optional=optional)
 9081
 9082    def _parse_wrapped_csv(
 9083        self,
 9084        parse_method: t.Callable[[], T | None],
 9085        sep: TokenType = TokenType.COMMA,
 9086        optional: bool = False,
 9087    ) -> list[T]:
 9088        return self._parse_wrapped(
 9089            lambda: self._parse_csv(parse_method, sep=sep), optional=optional
 9090        )
 9091
 9092    def _parse_wrapped(self, parse_method: t.Callable[[], T], optional: bool = False) -> T:
 9093        wrapped = self._match(TokenType.L_PAREN)
 9094        if not wrapped and not optional:
 9095            self.raise_error("Expecting (")
 9096        parse_result = parse_method()
 9097        if wrapped:
 9098            self._match_r_paren()
 9099        return parse_result
 9100
 9101    def _parse_expressions(self) -> list[exp.Expr]:
 9102        return self._parse_csv(self._parse_expression)
 9103
 9104    def _parse_select_or_expression(self, alias: bool = False) -> exp.Expr | None:
 9105        return (
 9106            self._parse_set_operations(
 9107                self._parse_alias(self._parse_assignment(), explicit=True)
 9108                if alias
 9109                else self._parse_assignment()
 9110            )
 9111            or self._parse_select()
 9112        )
 9113
 9114    def _parse_ddl_select(self) -> exp.Expr | None:
 9115        return self._parse_query_modifiers(
 9116            self._parse_set_operations(self._parse_select(nested=True, parse_subquery_alias=False))
 9117        )
 9118
 9119    def _parse_transaction(self) -> exp.Transaction | exp.Command:
 9120        this = None
 9121        if self._match_texts(self.TRANSACTION_KIND):
 9122            this = self._prev.text
 9123
 9124        self._match_texts(("TRANSACTION", "WORK"))
 9125
 9126        modes = []
 9127        while True:
 9128            mode = []
 9129            while self._match(TokenType.VAR) or self._match(TokenType.NOT):
 9130                mode.append(self._prev.text)
 9131
 9132            if mode:
 9133                modes.append(" ".join(mode))
 9134            if not self._match(TokenType.COMMA):
 9135                break
 9136
 9137        return self.expression(exp.Transaction(this=this, modes=modes))
 9138
 9139    def _parse_commit_or_rollback(self) -> exp.Commit | exp.Rollback:
 9140        chain = None
 9141        savepoint = None
 9142        is_rollback = self._prev.token_type == TokenType.ROLLBACK
 9143
 9144        self._match_texts(("TRANSACTION", "WORK"))
 9145
 9146        if self._match_text_seq("TO"):
 9147            self._match_text_seq("SAVEPOINT")
 9148            savepoint = self._parse_id_var()
 9149
 9150        if self._match(TokenType.AND):
 9151            chain = not self._match_text_seq("NO")
 9152            self._match_text_seq("CHAIN")
 9153
 9154        if is_rollback:
 9155            return self.expression(exp.Rollback(savepoint=savepoint))
 9156
 9157        return self.expression(exp.Commit(chain=chain))
 9158
 9159    def _parse_refresh(self) -> exp.Refresh | exp.Command:
 9160        if self._match_text_seq("EXTERNAL", "TABLE"):
 9161            kind = "EXTERNAL TABLE"
 9162        elif self._match(TokenType.TABLE):
 9163            kind = "TABLE"
 9164        elif self._match_text_seq("MATERIALIZED", "VIEW"):
 9165            kind = "MATERIALIZED VIEW"
 9166        else:
 9167            kind = ""
 9168
 9169        this = self._parse_string() or self._parse_table()
 9170        if not kind and not isinstance(this, exp.Literal):
 9171            return self._parse_as_command(self._prev)
 9172
 9173        return self.expression(exp.Refresh(this=this, kind=kind))
 9174
 9175    def _parse_column_def_with_exists(self):
 9176        start = self._index
 9177        self._match(TokenType.COLUMN)
 9178
 9179        exists_column = self._parse_exists(not_=True)
 9180        expression = self._parse_field_def()
 9181
 9182        if not isinstance(expression, exp.ColumnDef):
 9183            self._retreat(start)
 9184            return None
 9185
 9186        expression.set("exists", exists_column)
 9187
 9188        return expression
 9189
 9190    def _parse_add_column(self) -> exp.ColumnDef | None:
 9191        if not self._prev.text.upper() == "ADD":
 9192            return None
 9193
 9194        return self._parse_column_def_with_exists()
 9195
 9196    def _parse_drop_column(self) -> exp.Drop | exp.Command | None:
 9197        if not self._match(TokenType.DROP):
 9198            return None
 9199
 9200        kind = None
 9201        if not self.ALTER_DROP_REQUIRES_COLUMN:
 9202            # A bare `DROP [IF EXISTS] c [CASCADE | RESTRICT]` drops a column
 9203            index = self._index
 9204            self._parse_exists()
 9205            if (
 9206                self._curr.token_type not in (TokenType.COLUMN, TokenType.CONSTRAINT)
 9207                and self._match_set(self.ID_VAR_TOKENS)
 9208                and (
 9209                    not self._curr
 9210                    or self._match(TokenType.COMMA, advance=False)
 9211                    or self._match_texts(("CASCADE", "RESTRICT"), advance=False)
 9212                )
 9213            ):
 9214                kind = "COLUMN"
 9215            self._retreat(index)
 9216
 9217        return self._parse_drop(kind=kind)
 9218
 9219    def _parse_alter_drop_action(self) -> exp.Expr | None:
 9220        drop = self._parse_drop_column()
 9221
 9222        if (
 9223            isinstance(drop, exp.Drop)
 9224            and drop.kind == "COLUMN"
 9225            and not self.dialect.ALTER_TABLE_DROP_REQUIRED_FOR_EACH_COLUMN
 9226            and self._match(TokenType.COMMA)
 9227        ):
 9228            drop.set("tables", [*drop.args["tables"], *self._parse_csv(self._parse_column)])
 9229
 9230        return drop
 9231
 9232    # https://docs.aws.amazon.com/athena/latest/ug/alter-table-drop-partition.html
 9233    def _parse_drop_partition(self, exists: bool | None = None) -> exp.DropPartition:
 9234        return self.expression(
 9235            exp.DropPartition(expressions=self._parse_csv(self._parse_partition), exists=exists)
 9236        )
 9237
 9238    def _parse_alter_table_add(self) -> list[exp.Expr]:
 9239        def _parse_add_alteration() -> exp.Expr | None:
 9240            self._match_text_seq("ADD")
 9241            if self._match_set(self.ADD_CONSTRAINT_TOKENS, advance=False) or self._match_texts(
 9242                self.ADD_CONSTRAINT_KEYWORDS, advance=False
 9243            ):
 9244                return self.expression(
 9245                    exp.AddConstraint(expressions=self._parse_csv(self._parse_constraint))
 9246                )
 9247
 9248            column_def = self._parse_add_column()
 9249            if isinstance(column_def, exp.ColumnDef):
 9250                return column_def
 9251
 9252            exists = self._parse_exists(not_=True)
 9253            if self._match_pair(TokenType.PARTITION, TokenType.L_PAREN, advance=False):
 9254                return self.expression(
 9255                    exp.AddPartition(
 9256                        exists=exists,
 9257                        this=self._parse_field(any_token=True),
 9258                        location=self._match_text_seq("LOCATION", advance=False)
 9259                        and self._parse_property(),
 9260                    )
 9261                )
 9262
 9263            return None
 9264
 9265        if not self._match_set(self.ADD_CONSTRAINT_TOKENS, advance=False) and (
 9266            not self.dialect.ALTER_TABLE_ADD_REQUIRED_FOR_EACH_COLUMN
 9267            or self._match_text_seq("COLUMNS")
 9268        ):
 9269            schema = self._parse_schema()
 9270
 9271            return (
 9272                ensure_list(schema)
 9273                if schema
 9274                else self._parse_csv(self._parse_column_def_with_exists)
 9275            )
 9276
 9277        return self._parse_csv(_parse_add_alteration)
 9278
 9279    def _parse_alter_table_alter(self) -> exp.Expr | None:
 9280        if self._match_texts(self.ALTER_ALTER_PARSERS):
 9281            return self.ALTER_ALTER_PARSERS[self._prev.text.upper()](self)
 9282
 9283        # Many dialects support the ALTER [COLUMN] syntax, so if there is no
 9284        # keyword after ALTER we default to parsing this statement
 9285        self._match(TokenType.COLUMN)
 9286        exists = self._parse_exists()
 9287        column = self._parse_field(any_token=True)
 9288
 9289        if self._match_pair(TokenType.DROP, TokenType.DEFAULT):
 9290            return self.expression(exp.AlterColumn(this=column, drop=True, exists=exists or None))
 9291        if self._match_pair(TokenType.SET, TokenType.DEFAULT):
 9292            return self.expression(
 9293                exp.AlterColumn(
 9294                    this=column, default=self._parse_disjunction(), exists=exists or None
 9295                )
 9296            )
 9297        if self._match(TokenType.COMMENT):
 9298            return self.expression(
 9299                exp.AlterColumn(this=column, comment=self._parse_string(), exists=exists or None)
 9300            )
 9301        if self._match_text_seq("DROP", "NOT", "NULL"):
 9302            return self.expression(
 9303                exp.AlterColumn(this=column, drop=True, allow_null=True, exists=exists or None)
 9304            )
 9305        if self._match_text_seq("SET", "NOT", "NULL"):
 9306            return self.expression(
 9307                exp.AlterColumn(this=column, allow_null=False, exists=exists or None)
 9308            )
 9309
 9310        if self._match_text_seq("SET", "VISIBLE"):
 9311            return self.expression(
 9312                exp.AlterColumn(this=column, visible="VISIBLE", exists=exists or None)
 9313            )
 9314        if self._match_text_seq("SET", "INVISIBLE"):
 9315            return self.expression(
 9316                exp.AlterColumn(this=column, visible="INVISIBLE", exists=exists or None)
 9317            )
 9318
 9319        self._match_text_seq("SET", "DATA")
 9320        dtype = (
 9321            self._parse_types()
 9322            if self._match_text_seq("TYPE") or not self.ALTER_COLUMN_TYPE_REQUIRES_KEYWORD
 9323            else None
 9324        )
 9325        if not dtype:
 9326            return None
 9327
 9328        return self.expression(
 9329            exp.AlterColumn(
 9330                this=column,
 9331                dtype=dtype,
 9332                collate=self._match(TokenType.COLLATE) and self._parse_term(),
 9333                using=self._match(TokenType.USING) and self._parse_disjunction(),
 9334                exists=exists or None,
 9335            )
 9336        )
 9337
 9338    def _parse_alter_diststyle(self) -> exp.AlterDistStyle:
 9339        if self._match_texts(("ALL", "EVEN", "AUTO")):
 9340            return self.expression(exp.AlterDistStyle(this=exp.var(self._prev.text.upper())))
 9341
 9342        self._match_text_seq("KEY", "DISTKEY")
 9343        return self.expression(exp.AlterDistStyle(this=self._parse_column()))
 9344
 9345    def _parse_alter_sortkey(self, compound: bool | None = None) -> exp.AlterSortKey:
 9346        if compound:
 9347            self._match_text_seq("SORTKEY")
 9348
 9349        if self._match(TokenType.L_PAREN, advance=False):
 9350            return self.expression(
 9351                exp.AlterSortKey(expressions=self._parse_wrapped_id_vars(), compound=compound)
 9352            )
 9353
 9354        self._match_texts(("AUTO", "NONE"))
 9355        return self.expression(
 9356            exp.AlterSortKey(this=exp.var(self._prev.text.upper()), compound=compound)
 9357        )
 9358
 9359    def _parse_alter_table_drop(self) -> list[exp.Expr]:
 9360        index = self._index - 1
 9361
 9362        partition_exists = self._parse_exists()
 9363        if self._match(TokenType.PARTITION, advance=False):
 9364            return self._parse_csv(lambda: self._parse_drop_partition(exists=partition_exists))
 9365
 9366        self._retreat(index)
 9367        return self._parse_csv(self._parse_alter_drop_action)
 9368
 9369    def _parse_alter_table_rename(self) -> exp.AlterRename | exp.RenameColumn | None:
 9370        if self._match(TokenType.COLUMN) or (
 9371            not self.ALTER_RENAME_REQUIRES_COLUMN and not self._match_text_seq("TO", advance=False)
 9372        ):
 9373            exists = self._parse_exists()
 9374            old_column = self._parse_column()
 9375            to = self._match_text_seq("TO")
 9376            new_column = self._parse_column()
 9377
 9378            if old_column is None or not to or new_column is None:
 9379                return None
 9380
 9381            return self.expression(exp.RenameColumn(this=old_column, to=new_column, exists=exists))
 9382
 9383        self._match_text_seq("TO")
 9384        return self.expression(exp.AlterRename(this=self._parse_table(schema=True)))
 9385
 9386    def _parse_alter_table_set(self) -> exp.AlterSet:
 9387        alter_set = self.expression(exp.AlterSet())
 9388
 9389        if self._match(TokenType.L_PAREN, advance=False) or self._match_text_seq(
 9390            "TABLE", "PROPERTIES"
 9391        ):
 9392            alter_set.set("expressions", self._parse_wrapped_csv(self._parse_assignment))
 9393        elif self._match_text_seq("FILESTREAM_ON", advance=False):
 9394            alter_set.set("expressions", [self._parse_assignment()])
 9395        elif self._match_texts(("LOGGED", "UNLOGGED")):
 9396            alter_set.set("option", exp.var(self._prev.text.upper()))
 9397        elif self._match_text_seq("WITHOUT") and self._match_texts(("CLUSTER", "OIDS")):
 9398            alter_set.set("option", exp.var(f"WITHOUT {self._prev.text.upper()}"))
 9399        elif self._match_text_seq("LOCATION"):
 9400            alter_set.set("location", self._parse_field())
 9401        elif self._match_text_seq("ACCESS", "METHOD"):
 9402            alter_set.set("access_method", self._parse_field())
 9403        elif self._match_text_seq("TABLESPACE"):
 9404            alter_set.set("tablespace", self._parse_field())
 9405        elif self._match_text_seq("FILE", "FORMAT") or self._match_text_seq("FILEFORMAT"):
 9406            alter_set.set("file_format", [self._parse_field()])
 9407        elif self._match_text_seq("STAGE_FILE_FORMAT"):
 9408            alter_set.set("file_format", self._parse_wrapped_options())
 9409        elif self._match_text_seq("STAGE_COPY_OPTIONS"):
 9410            alter_set.set("copy_options", self._parse_wrapped_options())
 9411        elif self._match_text_seq("TAG") or self._match_text_seq("TAGS"):
 9412            alter_set.set("tag", self._parse_csv(self._parse_assignment))
 9413        else:
 9414            if self._match_text_seq("SERDE"):
 9415                alter_set.set("serde", self._parse_field())
 9416
 9417            properties = self._parse_wrapped(self._parse_properties, optional=True)
 9418            alter_set.set("expressions", [properties])
 9419
 9420        return alter_set
 9421
 9422    def _parse_alter_session(self) -> exp.AlterSession:
 9423        """Parse ALTER SESSION SET/UNSET statements."""
 9424        if self._match(TokenType.SET):
 9425            expressions = self._parse_csv(lambda: self._parse_set_item_assignment())
 9426            return self.expression(exp.AlterSession(expressions=expressions, unset=False))
 9427
 9428        self._match_text_seq("UNSET")
 9429        expressions = self._parse_csv(
 9430            lambda: self.expression(exp.SetItem(this=self._parse_id_var(any_token=True)))
 9431        )
 9432        return self.expression(exp.AlterSession(expressions=expressions, unset=True))
 9433
 9434    def _parse_alter(self) -> exp.Alter | exp.Command:
 9435        start = self._prev
 9436
 9437        iceberg = self._match_text_seq("ICEBERG")
 9438
 9439        alter_token = self._match_set(self.ALTERABLES) and self._prev
 9440        if not alter_token:
 9441            return self._parse_as_command(start)
 9442        if iceberg and alter_token.token_type != TokenType.TABLE:
 9443            return self._parse_as_command(start)
 9444
 9445        exists = self._parse_exists()
 9446        only = self._match_text_seq("ONLY")
 9447
 9448        if alter_token.token_type == TokenType.SESSION:
 9449            this = None
 9450            check = None
 9451            cluster = None
 9452            parser = self.ALTER_PARSERS.get(alter_token.text.upper())
 9453        else:
 9454            this = self._parse_table(schema=True, parse_partition=self.ALTER_TABLE_PARTITIONS)
 9455            check = self._match_text_seq("WITH", "CHECK")
 9456            cluster = self._parse_on_property() if self._match(TokenType.ON) else None
 9457            parser = (
 9458                self.ALTER_PARSERS[self._prev.text.upper()]
 9459                if self._match_texts(self.ALTER_PARSERS)
 9460                else None
 9461            )
 9462
 9463        options: list[exp.Expr] = []
 9464        if (
 9465            not parser
 9466            and alter_token.token_type == TokenType.TABLE
 9467            and not self.ALTER_TABLE_REQUIRES_ACTION
 9468        ):
 9469            # Table options may precede the actions, e.g. MySQL's ENGINE=InnoDB, ADD COLUMN a INT.
 9470            # A clause the property parser rejects, like PARTITION BY RANGE ... MAXVALUE, stays a Command
 9471            properties = self._try_parse(self._parse_properties)
 9472            options = properties.expressions if properties else []
 9473
 9474        actions = ensure_list(parser(self) if parser else None)
 9475        if actions or options:
 9476            # Actions of different kinds and table options may alternate, e.g. MySQL's
 9477            # ADD c INT, AUTO_INCREMENT=5, ADD d INT. The comma before an item may have already
 9478            # been consumed by the previous parser
 9479            while self.ALTER_TABLE_MIXED_ACTIONS and (
 9480                self._match(TokenType.COMMA) or self._prev.token_type == TokenType.COMMA
 9481            ):
 9482                if not self._match_texts(
 9483                    self.PROPERTY_PARSERS, advance=False
 9484                ) and self._match_texts(self.ALTER_PARSERS):
 9485                    parser = self.ALTER_PARSERS[self._prev.text.upper()]
 9486                    parsed = ensure_list(self._try_parse(lambda: parser(self)))
 9487                    if not parsed or any(isinstance(action, exp.Command) for action in parsed):
 9488                        return self._parse_as_command(start)
 9489
 9490                    actions.extend(parsed)
 9491                elif properties := self._try_parse(self._parse_properties):
 9492                    options.extend(properties.expressions)
 9493                else:
 9494                    break
 9495
 9496            not_valid = self._match_text_seq("NOT", "VALID")
 9497            if properties := self._try_parse(self._parse_properties):
 9498                options.extend(properties.expressions)
 9499            cascade = self.dialect.ALTER_TABLE_SUPPORTS_CASCADE and self._match_text_seq("CASCADE")
 9500
 9501            if not self._curr:
 9502                return self.expression(
 9503                    exp.Alter(
 9504                        this=this,
 9505                        kind=alter_token.text.upper(),
 9506                        exists=exists,
 9507                        actions=actions,
 9508                        only=only,
 9509                        options=options,
 9510                        cluster=cluster,
 9511                        not_valid=not_valid,
 9512                        check=check,
 9513                        cascade=cascade,
 9514                        iceberg=iceberg,
 9515                    )
 9516                )
 9517
 9518        return self._parse_as_command(start)
 9519
 9520    def _parse_analyze(self) -> exp.Analyze | exp.Command:
 9521        start = self._prev
 9522        # https://duckdb.org/docs/sql/statements/analyze
 9523        if not self._curr:
 9524            return self.expression(exp.Analyze())
 9525
 9526        options = []
 9527        while self._match_texts(self.ANALYZE_STYLES):
 9528            if self._prev.text.upper() == "BUFFER_USAGE_LIMIT":
 9529                options.append(f"BUFFER_USAGE_LIMIT {self._parse_number()}")
 9530            else:
 9531                options.append(self._prev.text.upper())
 9532
 9533        tables: exp.Expr | list[exp.Expr] | None = None
 9534        inner_expression: exp.Expr | None = None
 9535
 9536        kind = self._curr.text.upper() if self._curr else None
 9537
 9538        if self._match(TokenType.TABLE):
 9539            tables = self._parse_csv(self._parse_table_parts)
 9540        elif self._match(TokenType.INDEX):
 9541            tables = self._parse_table_parts()
 9542        elif self._match_text_seq("TABLES"):
 9543            if self._match_set((TokenType.FROM, TokenType.IN)):
 9544                kind = f"{kind} {self._prev.text.upper()}"
 9545                tables = self._parse_table(schema=True, is_db_reference=True)
 9546        elif self._match_text_seq("DATABASE"):
 9547            tables = self._parse_table(schema=True, is_db_reference=True)
 9548        elif self._match_text_seq("CLUSTER"):
 9549            tables = self._parse_table()
 9550        # Try matching inner expr keywords before fallback to parse table.
 9551        elif self._match_texts(self.ANALYZE_EXPRESSION_PARSERS):
 9552            kind = None
 9553            inner_expression = self.ANALYZE_EXPRESSION_PARSERS[self._prev.text.upper()](self)
 9554        else:
 9555            # Empty kind  https://prestodb.io/docs/current/sql/analyze.html
 9556            kind = None
 9557            tables = self._parse_csv(self._parse_table_parts)
 9558
 9559        partition = self._try_parse(self._parse_partition)
 9560        if not partition and self._match_texts(self.PARTITION_KEYWORDS):
 9561            return self._parse_as_command(start)
 9562
 9563        # https://docs.starrocks.io/docs/sql-reference/sql-statements/cbo_stats/ANALYZE_TABLE/
 9564        if self._match_text_seq("WITH", "SYNC", "MODE") or self._match_text_seq(
 9565            "WITH", "ASYNC", "MODE"
 9566        ):
 9567            mode = f"WITH {self._tokens[self._index - 2].text.upper()} MODE"
 9568        else:
 9569            mode = None
 9570
 9571        if self._match_texts(self.ANALYZE_EXPRESSION_PARSERS):
 9572            inner_expression = self.ANALYZE_EXPRESSION_PARSERS[self._prev.text.upper()](self)
 9573
 9574        properties = self._parse_properties()
 9575        return self.expression(
 9576            exp.Analyze(
 9577                kind=kind,
 9578                tables=ensure_list(tables),
 9579                mode=mode,
 9580                partition=partition,
 9581                properties=properties,
 9582                expression=inner_expression,
 9583                options=options,
 9584            )
 9585        )
 9586
 9587    # https://spark.apache.org/docs/3.5.1/sql-ref-syntax-aux-analyze-table.html
 9588    def _parse_analyze_statistics(self) -> exp.AnalyzeStatistics:
 9589        this = None
 9590        kind = self._prev.text.upper()
 9591        option = self._prev.text.upper() if self._match_text_seq("DELTA") else None
 9592        expressions = []
 9593
 9594        if not self._match_text_seq("STATISTICS"):
 9595            self.raise_error("Expecting token STATISTICS")
 9596
 9597        if self._match_text_seq("NOSCAN"):
 9598            this = "NOSCAN"
 9599        elif self._match(TokenType.FOR):
 9600            if self._match_text_seq("ALL", "COLUMNS"):
 9601                this = "FOR ALL COLUMNS"
 9602            if self._match_text_seq("COLUMNS"):
 9603                this = "FOR COLUMNS"
 9604                expressions = self._parse_csv(self._parse_column_reference)
 9605        elif self._match_text_seq("SAMPLE"):
 9606            sample = self._parse_number()
 9607            expressions = [
 9608                self.expression(
 9609                    exp.AnalyzeSample(
 9610                        sample=sample,
 9611                        kind=self._prev.text.upper() if self._match(TokenType.PERCENT) else None,
 9612                    )
 9613                )
 9614            ]
 9615
 9616        return self.expression(
 9617            exp.AnalyzeStatistics(kind=kind, option=option, this=this, expressions=expressions)
 9618        )
 9619
 9620    # https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/ANALYZE.html
 9621    def _parse_analyze_validate(self) -> exp.AnalyzeValidate:
 9622        kind = None
 9623        this = None
 9624        expression: exp.Expr | None = None
 9625        if self._match_text_seq("REF", "UPDATE"):
 9626            kind = "REF"
 9627            this = "UPDATE"
 9628            if self._match_text_seq("SET", "DANGLING", "TO", "NULL"):
 9629                this = "UPDATE SET DANGLING TO NULL"
 9630        elif self._match_text_seq("STRUCTURE"):
 9631            kind = "STRUCTURE"
 9632            if self._match_text_seq("CASCADE", "FAST"):
 9633                this = "CASCADE FAST"
 9634            elif self._match_text_seq("CASCADE", "COMPLETE") and self._match_texts(
 9635                ("ONLINE", "OFFLINE")
 9636            ):
 9637                this = f"CASCADE COMPLETE {self._prev.text.upper()}"
 9638                expression = self._parse_into()
 9639
 9640        return self.expression(exp.AnalyzeValidate(kind=kind, this=this, expression=expression))
 9641
 9642    def _parse_analyze_columns(self) -> exp.AnalyzeColumns | None:
 9643        this = self._prev.text.upper()
 9644        if self._match_text_seq("COLUMNS"):
 9645            return self.expression(exp.AnalyzeColumns(this=f"{this} {self._prev.text.upper()}"))
 9646        return None
 9647
 9648    def _parse_analyze_delete(self) -> exp.AnalyzeDelete | None:
 9649        kind = self._prev.text.upper() if self._match_text_seq("SYSTEM") else None
 9650        if self._match_text_seq("STATISTICS"):
 9651            return self.expression(exp.AnalyzeDelete(kind=kind))
 9652        return None
 9653
 9654    def _parse_analyze_list(self) -> exp.AnalyzeListChainedRows | None:
 9655        if self._match_text_seq("CHAINED", "ROWS"):
 9656            return self.expression(exp.AnalyzeListChainedRows(expression=self._parse_into()))
 9657        return None
 9658
 9659    # https://dev.mysql.com/doc/refman/8.4/en/analyze-table.html
 9660    def _parse_analyze_histogram(self) -> exp.AnalyzeHistogram:
 9661        this = self._prev.text.upper()
 9662        expression: exp.Expr | None = None
 9663        expressions = []
 9664        update_options = None
 9665
 9666        if self._match_text_seq("HISTOGRAM", "ON"):
 9667            expressions = self._parse_csv(self._parse_column_reference)
 9668            with_expressions = []
 9669            while self._match(TokenType.WITH):
 9670                # https://docs.starrocks.io/docs/sql-reference/sql-statements/cbo_stats/ANALYZE_TABLE/
 9671                if self._match_texts(("SYNC", "ASYNC")):
 9672                    if self._match_text_seq("MODE", advance=False):
 9673                        with_expressions.append(f"{self._prev.text.upper()} MODE")
 9674                        self._advance()
 9675                else:
 9676                    buckets = self._parse_number()
 9677                    if self._match_text_seq("BUCKETS"):
 9678                        with_expressions.append(f"{buckets} BUCKETS")
 9679            if with_expressions:
 9680                expression = self.expression(exp.AnalyzeWith(expressions=with_expressions))
 9681
 9682            if self._match_texts(("MANUAL", "AUTO")) and self._match(
 9683                TokenType.UPDATE, advance=False
 9684            ):
 9685                update_options = self._prev.text.upper()
 9686                self._advance()
 9687            elif self._match_text_seq("USING", "DATA"):
 9688                expression = self.expression(exp.UsingData(this=self._parse_string()))
 9689
 9690        return self.expression(
 9691            exp.AnalyzeHistogram(
 9692                this=this,
 9693                expressions=expressions,
 9694                expression=expression,
 9695                update_options=update_options,
 9696            )
 9697        )
 9698
 9699    def _parse_merge(self) -> exp.Merge:
 9700        self._match(TokenType.INTO)
 9701        target = self._parse_table()
 9702
 9703        if target and self._match(TokenType.ALIAS, advance=False):
 9704            target.set("alias", self._parse_table_alias())
 9705
 9706        self._match(TokenType.USING)
 9707        using = self._parse_table()
 9708
 9709        return self.expression(
 9710            exp.Merge(
 9711                this=target,
 9712                using=using,
 9713                on=self._match(TokenType.ON) and self._parse_disjunction(),
 9714                using_cond=self._match(TokenType.USING) and self._parse_using_identifiers(),
 9715                whens=self._parse_when_matched(),
 9716                returning=self._parse_returning(),
 9717            )
 9718        )
 9719
 9720    def _parse_when_matched(self) -> exp.Whens:
 9721        whens = []
 9722
 9723        while self._match(TokenType.WHEN):
 9724            matched = not self._match(TokenType.NOT)
 9725            self._match_text_seq("MATCHED")
 9726            source = (
 9727                False
 9728                if self._match_text_seq("BY", "TARGET")
 9729                else self._match_text_seq("BY", "SOURCE")
 9730            )
 9731            condition = self._parse_disjunction() if self._match(TokenType.AND) else None
 9732
 9733            self._match(TokenType.THEN)
 9734
 9735            if self._match(TokenType.INSERT):
 9736                this = self._parse_star()
 9737                if this:
 9738                    then: exp.Expr | None = self.expression(exp.Insert(this=this))
 9739                else:
 9740                    then = self.expression(
 9741                        exp.Insert(
 9742                            this=exp.var("ROW")
 9743                            if self._match_text_seq("ROW")
 9744                            else self._parse_value(values=False),
 9745                            expression=self._match_text_seq("VALUES") and self._parse_value(),
 9746                            where=self._parse_where(),
 9747                        )
 9748                    )
 9749            elif self._match(TokenType.UPDATE):
 9750                expressions = self._parse_star()
 9751                if expressions:
 9752                    then = self.expression(exp.Update(expressions=expressions))
 9753                else:
 9754                    then = self.expression(
 9755                        exp.Update(
 9756                            expressions=self._match(TokenType.SET)
 9757                            and self._parse_csv(self._parse_update_assignment),
 9758                            where=self._parse_where(),
 9759                        )
 9760                    )
 9761            elif self._match(TokenType.DELETE):
 9762                then = self.expression(exp.Var(this=self._prev.text))
 9763            else:
 9764                then = self._parse_var_from_options(self.CONFLICT_ACTIONS)
 9765
 9766            whens.append(
 9767                self.expression(
 9768                    exp.When(matched=matched, source=source, condition=condition, then=then)
 9769                )
 9770            )
 9771        return self.expression(exp.Whens(expressions=whens))
 9772
 9773    def _parse_show(self) -> exp.Expr | None:
 9774        parser = self._find_parser(self.SHOW_PARSERS, self.SHOW_TRIE)
 9775        if parser:
 9776            return parser(self)
 9777        return self._parse_as_command(self._prev)
 9778
 9779    def _parse_set_item_assignment(self, kind: str | None = None) -> exp.Expr | None:
 9780        index = self._index
 9781
 9782        if kind in ("GLOBAL", "SESSION") and self._match_text_seq("TRANSACTION"):
 9783            return self._parse_set_transaction(global_=kind == "GLOBAL")
 9784
 9785        left = self._parse_primary() or self._parse_column()
 9786        assignment_delimiter = self._match_texts(self.SET_ASSIGNMENT_DELIMITERS)
 9787
 9788        if not left or (self.SET_REQUIRES_ASSIGNMENT_DELIMITER and not assignment_delimiter):
 9789            self._retreat(index)
 9790            return None
 9791
 9792        right = self._parse_statement() or self._parse_id_var()
 9793        if isinstance(right, (exp.Column, exp.Identifier)):
 9794            right = exp.var(right.name)
 9795
 9796        this = self.expression(exp.EQ(this=left, expression=right))
 9797        return self.expression(exp.SetItem(this=this, kind=kind))
 9798
 9799    def _parse_set_transaction(self, global_: bool = False) -> exp.Expr:
 9800        self._match_text_seq("TRANSACTION")
 9801        characteristics = self._parse_csv(
 9802            lambda: self._parse_var_from_options(self.TRANSACTION_CHARACTERISTICS)
 9803        )
 9804        return self.expression(
 9805            exp.SetItem(expressions=characteristics, kind="TRANSACTION", global_=global_)
 9806        )
 9807
 9808    def _parse_set_item(self) -> exp.Expr | None:
 9809        parser = self._find_parser(self.SET_PARSERS, self.SET_TRIE)
 9810        return parser(self) if parser else self._parse_set_item_assignment(kind=None)
 9811
 9812    def _parse_set(self, unset: bool = False, tag: bool = False) -> exp.Set | exp.Command:
 9813        index = self._index
 9814        set_ = self.expression(
 9815            exp.Set(expressions=self._parse_csv(self._parse_set_item), unset=unset, tag=tag)
 9816        )
 9817
 9818        if self._curr:
 9819            self._retreat(index)
 9820            return self._parse_as_command(self._prev)
 9821
 9822        return set_
 9823
 9824    def _parse_var_from_options(
 9825        self, options: OPTIONS_TYPE, raise_unmatched: bool = True
 9826    ) -> exp.Var | None:
 9827        start = self._curr
 9828        if not start:
 9829            return None
 9830
 9831        option = start.text.upper()
 9832        continuations = (
 9833            None if start.token_type in self.TEXT_MATCH_EXCLUDED_TOKENS else options.get(option)
 9834        )
 9835
 9836        index = self._index
 9837        self._advance()
 9838        for keywords in continuations or []:
 9839            if isinstance(keywords, str):
 9840                keywords = (keywords,)
 9841
 9842            if self._match_text_seq(*keywords):
 9843                option = f"{option} {' '.join(keywords)}"
 9844                break
 9845        else:
 9846            if continuations or continuations is None:
 9847                if raise_unmatched:
 9848                    self.raise_error(f"Unknown option {option}")
 9849
 9850                self._retreat(index)
 9851                return None
 9852
 9853        return exp.var(option)
 9854
 9855    def _parse_as_command(self, start: Token) -> exp.Command:
 9856        while self._curr:
 9857            self._advance()
 9858        text = self._find_sql(start, self._prev)
 9859        size = len(start.text)
 9860        self._warn_unsupported()
 9861        return exp.Command(this=text[:size], expression=text[size:])
 9862
 9863    def _parse_dict_property(self, this: str) -> exp.DictProperty:
 9864        settings = []
 9865
 9866        self._match_l_paren()
 9867        kind = self._parse_id_var()
 9868
 9869        if self._match(TokenType.L_PAREN):
 9870            while True:
 9871                key = self._parse_id_var()
 9872                value = self._parse_function() or self._parse_primary_or_var()
 9873                if not key and value is None:
 9874                    break
 9875                settings.append(self.expression(exp.DictSubProperty(this=key, value=value)))
 9876            self._match(TokenType.R_PAREN)
 9877
 9878        self._match_r_paren()
 9879
 9880        return self.expression(
 9881            exp.DictProperty(this=this, kind=kind.this if kind else None, settings=settings)
 9882        )
 9883
 9884    def _parse_dict_range(self, this: str) -> exp.DictRange:
 9885        self._match_l_paren()
 9886        has_min = self._match_text_seq("MIN")
 9887        if has_min:
 9888            min = self._parse_var() or self._parse_primary()
 9889            self._match_text_seq("MAX")
 9890            max = self._parse_var() or self._parse_primary()
 9891        else:
 9892            max = self._parse_var() or self._parse_primary()
 9893            min = exp.Literal.number(0)
 9894        self._match_r_paren()
 9895        return self.expression(exp.DictRange(this=this, min=min, max=max))
 9896
 9897    def _parse_comprehension(self, this: exp.Expr | None) -> exp.Comprehension | None:
 9898        index = self._index
 9899        expression = self._parse_id_var()
 9900        position = self._parse_id_var() if self._match(TokenType.COMMA) else None
 9901
 9902        if not self._match(TokenType.IN):
 9903            self._retreat(index - 1)
 9904            return None
 9905        iterator = self._parse_column()
 9906        condition = self._parse_disjunction() if self._match_text_seq("IF") else None
 9907        return self.expression(
 9908            exp.Comprehension(
 9909                this=this,
 9910                expression=expression,
 9911                position=position,
 9912                iterator=iterator,
 9913                condition=condition,
 9914            )
 9915        )
 9916
 9917    def _parse_heredoc(self) -> exp.Heredoc | None:
 9918        if self._match(TokenType.HEREDOC_STRING):
 9919            return self.expression(exp.Heredoc(this=self._prev.text))
 9920
 9921        if not self._match_text_seq("$"):
 9922            return None
 9923
 9924        tags = ["$"]
 9925        tag_text = None
 9926
 9927        if self._is_connected():
 9928            self._advance()
 9929            tags.append(self._prev.text.upper())
 9930        else:
 9931            self.raise_error("No closing $ found")
 9932
 9933        if tags[-1] != "$":
 9934            if self._is_connected() and self._match_text_seq("$"):
 9935                tag_text = tags[-1]
 9936                tags.append("$")
 9937            else:
 9938                self.raise_error("No closing $ found")
 9939
 9940        heredoc_start = self._curr
 9941
 9942        while self._curr:
 9943            if self._match_text_seq(*tags, advance=False):
 9944                this = self._find_sql(heredoc_start, self._prev)
 9945                self._advance(len(tags))
 9946                return self.expression(exp.Heredoc(this=this, tag=tag_text))
 9947
 9948            self._advance()
 9949
 9950        self.raise_error(f"No closing {''.join(tags)} found")
 9951        return None
 9952
 9953    def _find_parser(self, parsers: dict[str, t.Callable], trie: dict) -> t.Callable | None:
 9954        if not self._curr:
 9955            return None
 9956
 9957        index = self._index
 9958        this = []
 9959        while True:
 9960            # The current token might be multiple words
 9961            curr = self._curr.text.upper()
 9962            key = curr.split(" ")
 9963            this.append(curr)
 9964
 9965            self._advance()
 9966            result, trie = in_trie(trie, key)
 9967            if result == TrieResult.FAILED:
 9968                break
 9969
 9970            if result == TrieResult.EXISTS:
 9971                subparser = parsers[" ".join(this)]
 9972                return subparser
 9973
 9974        self._retreat(index)
 9975        return None
 9976
 9977    def _match_l_paren(self, expression: exp.Expr | None = None) -> None:
 9978        if not self._match(TokenType.L_PAREN, expression=expression):
 9979            self.raise_error("Expecting (")
 9980
 9981    def _match_r_paren(self, expression: exp.Expr | None = None) -> None:
 9982        if not self._match(TokenType.R_PAREN, expression=expression):
 9983            self.raise_error("Expecting )")
 9984
 9985    def _replace_lambda(
 9986        self, node: exp.Expr | None, expressions: list[exp.Expr]
 9987    ) -> exp.Expr | None:
 9988        if not node:
 9989            return node
 9990
 9991        lambda_types = {e.name: e.args.get("to") or False for e in expressions}
 9992
 9993        for column in node.find_all(exp.Column):
 9994            typ = lambda_types.get(column.parts[0].name)
 9995            if typ is not None:
 9996                dot_or_id = column.to_dot() if column.table else column.this
 9997
 9998                if typ:
 9999                    dot_or_id = self.expression(exp.Cast(this=dot_or_id, to=typ))
10000
10001                parent = column.parent
10002
10003                while isinstance(parent, exp.Dot):
10004                    if not isinstance(parent.parent, exp.Dot):
10005                        parent.replace(dot_or_id)
10006                        break
10007                    parent = parent.parent
10008                else:
10009                    if column is node:
10010                        node = dot_or_id
10011                    else:
10012                        column.replace(dot_or_id)
10013        return node
10014
10015    def _parse_truncate_table(self) -> exp.TruncateTable | None | exp.Expr:
10016        start = self._prev
10017
10018        # Not to be confused with TRUNCATE(number, decimals) function call
10019        if self._match(TokenType.L_PAREN):
10020            self._retreat(self._index - 2)
10021            return self._parse_function()
10022
10023        # Clickhouse supports TRUNCATE DATABASE as well
10024        is_database = self._match(TokenType.DATABASE)
10025
10026        self._match(TokenType.TABLE)
10027
10028        exists = self._parse_exists(not_=False)
10029
10030        expressions = self._parse_csv(
10031            lambda: self._parse_table(schema=True, is_db_reference=is_database)
10032        )
10033
10034        cluster = self._parse_on_property() if self._match(TokenType.ON) else None
10035
10036        if self._match_text_seq("RESTART", "IDENTITY"):
10037            identity = "RESTART"
10038        elif self._match_text_seq("CONTINUE", "IDENTITY"):
10039            identity = "CONTINUE"
10040        else:
10041            identity = None
10042
10043        if self._match_text_seq("CASCADE") or self._match_text_seq("RESTRICT"):
10044            option = self._prev.text
10045        else:
10046            option = None
10047
10048        partition = self._parse_partition()
10049
10050        # Fallback case
10051        if self._curr:
10052            return self._parse_as_command(start)
10053
10054        return self.expression(
10055            exp.TruncateTable(
10056                expressions=expressions,
10057                is_database=is_database,
10058                exists=exists,
10059                cluster=cluster,
10060                identity=identity,
10061                option=option,
10062                partition=partition,
10063            )
10064        )
10065
10066    def _parse_indexed_column(self) -> exp.Expr | None:
10067        return self._parse_ordered(self._parse_opclass)
10068
10069    def _parse_with_operator(self) -> exp.Expr | None:
10070        this = self._parse_indexed_column()
10071
10072        if not self._match(TokenType.WITH):
10073            return this
10074
10075        op = self._parse_var(any_token=True, tokens=self.RESERVED_TOKENS)
10076
10077        return self.expression(exp.WithOperator(this=this, op=op))
10078
10079    def _parse_wrapped_options(self) -> list[exp.Expr]:
10080        self._match(TokenType.EQ)
10081        self._match(TokenType.L_PAREN)
10082
10083        opts: list[exp.Expr] = []
10084        option: exp.Expr | list[exp.Expr] | None
10085        while self._curr and not self._match(TokenType.R_PAREN):
10086            if self._match_text_seq("FORMAT_NAME", "="):
10087                # The FORMAT_NAME can be set to an identifier for Snowflake and T-SQL
10088                option = self._parse_format_name()
10089            else:
10090                option = self._parse_property()
10091
10092            if option is None:
10093                self.raise_error("Unable to parse option")
10094                break
10095
10096            opts.extend(ensure_list(option))
10097
10098        return opts
10099
10100    def _parse_copy_parameters(self) -> list[exp.CopyParameter]:
10101        sep = TokenType.COMMA if self.dialect.COPY_PARAMS_ARE_CSV else None
10102
10103        options = []
10104        while self._curr and not self._match(TokenType.R_PAREN, advance=False):
10105            option = self._parse_var(any_token=True)
10106            prev = self._prev.text.upper()
10107
10108            # Different dialects might separate options and values by white space, "=" and "AS"
10109            self._match(TokenType.EQ)
10110            self._match(TokenType.ALIAS)
10111
10112            param = self.expression(exp.CopyParameter(this=option))
10113
10114            if prev in self.COPY_INTO_VARLEN_OPTIONS and self._match(
10115                TokenType.L_PAREN, advance=False
10116            ):
10117                # Snowflake FILE_FORMAT case, Databricks COPY & FORMAT options
10118                param.set("expressions", self._parse_wrapped_options())
10119            elif prev == "FILE_FORMAT":
10120                # T-SQL's external file format case
10121                param.set("expression", self._parse_field())
10122            elif (
10123                prev == "FORMAT"
10124                and self._prev.token_type == TokenType.ALIAS
10125                and self._match_texts(("AVRO", "JSON"))
10126            ):
10127                param.set("this", exp.var(f"FORMAT AS {self._prev.text.upper()}"))
10128                param.set("expression", self._parse_field())
10129            else:
10130                param.set("expression", self._parse_unquoted_field() or self._parse_bracket())
10131
10132            options.append(param)
10133
10134            if sep:
10135                self._match(sep)
10136
10137        return options
10138
10139    def _parse_credentials(self) -> exp.Credentials | None:
10140        expr = self.expression(exp.Credentials())
10141
10142        if self._match_text_seq("STORAGE_INTEGRATION", "="):
10143            expr.set("storage", self._parse_field())
10144        if self._match_text_seq("CREDENTIALS"):
10145            # Snowflake case: CREDENTIALS = (...), Redshift case: CREDENTIALS <string>
10146            creds = (
10147                self._parse_wrapped_options() if self._match(TokenType.EQ) else self._parse_field()
10148            )
10149            expr.set("credentials", creds)
10150        if self._match_text_seq("ENCRYPTION"):
10151            expr.set("encryption", self._parse_wrapped_options())
10152        if self._match_text_seq("IAM_ROLE"):
10153            expr.set(
10154                "iam_role",
10155                exp.var(self._prev.text) if self._match(TokenType.DEFAULT) else self._parse_field(),
10156            )
10157        if self._match_text_seq("REGION"):
10158            expr.set("region", self._parse_field())
10159
10160        return expr
10161
10162    def _parse_file_location(self) -> exp.Expr | None:
10163        return self._parse_field()
10164
10165    def _parse_copy(self) -> exp.Copy | exp.Command:
10166        start = self._prev
10167
10168        self._match(TokenType.INTO)
10169
10170        this = (
10171            self._parse_select(nested=True, parse_subquery_alias=False)
10172            if self._match(TokenType.L_PAREN, advance=False)
10173            else self._parse_table(schema=True)
10174        )
10175
10176        kind = self._match(TokenType.FROM) or not self._match_text_seq("TO")
10177
10178        files = self._parse_csv(self._parse_file_location)
10179        if self._match(TokenType.EQ, advance=False):
10180            # Backtrack one token since we've consumed the lhs of a parameter assignment here.
10181            # This can happen for Snowflake dialect. Instead, we'd like to parse the parameter
10182            # list via `_parse_wrapped(..)` below.
10183            self._advance(-1)
10184            files = []
10185
10186        credentials = self._parse_credentials()
10187
10188        self._match_text_seq("WITH")
10189
10190        params = self._parse_wrapped(self._parse_copy_parameters, optional=True)
10191
10192        # Fallback case
10193        if self._curr:
10194            return self._parse_as_command(start)
10195
10196        return self.expression(
10197            exp.Copy(this=this, kind=kind, credentials=credentials, files=files, params=params)
10198        )
10199
10200    def _parse_normalize(self) -> exp.Normalize:
10201        return self.expression(
10202            exp.Normalize(
10203                this=self._parse_bitwise(), form=self._match(TokenType.COMMA) and self._parse_var()
10204            )
10205        )
10206
10207    def _parse_ceil_floor(self, expr_type: type[TCeilFloor]) -> TCeilFloor:
10208        args = self._parse_csv(lambda: self._parse_lambda())
10209
10210        this = seq_get(args, 0)
10211        decimals = seq_get(args, 1)
10212
10213        return expr_type(
10214            this=this,
10215            decimals=decimals,
10216            to=self._parse_var() if self._match_text_seq("TO") else None,
10217        )
10218
10219    def _parse_star_ops(self) -> exp.Expr | None:
10220        star_token = self._prev
10221
10222        if self._match_text_seq("COLUMNS", "(", advance=False):
10223            this = self._parse_function()
10224            if isinstance(this, exp.Columns):
10225                this.set("unpack", True)
10226            return this
10227
10228        index = self._index
10229        ilike = self._parse_string() if self._match(TokenType.ILIKE) else None
10230        if not ilike:
10231            # ILIKE without a string pattern is not a star filter, e.g. `* ILIKE (foo)`
10232            self._retreat(index)
10233
10234        return self.expression(
10235            exp.Star(
10236                ilike=ilike,
10237                except_=self._parse_star_op("EXCEPT", "EXCLUDE"),
10238                replace=self._parse_star_op("REPLACE"),
10239                rename=self._parse_star_op("RENAME"),
10240            )
10241        ).update_positions(star_token)
10242
10243    def _parse_grant_privilege(self) -> exp.GrantPrivilege | None:
10244        privilege_parts = []
10245
10246        # Keep consuming consecutive keywords until comma (end of this privilege) or ON
10247        # (end of privilege list) or L_PAREN (start of column list) are met
10248        while self._curr and not self._match_set(self.PRIVILEGE_FOLLOW_TOKENS, advance=False):
10249            privilege_parts.append(self._curr.text.upper())
10250            self._advance()
10251
10252        if not privilege_parts:
10253            self.raise_error("Expected privilege")
10254            return None
10255
10256        this = exp.var(" ".join(privilege_parts))
10257        expressions = (
10258            self._parse_wrapped_csv(self._parse_column)
10259            if self._match(TokenType.L_PAREN, advance=False)
10260            else None
10261        )
10262
10263        return self.expression(exp.GrantPrivilege(this=this, expressions=expressions))
10264
10265    def _parse_grant_principal(self) -> exp.GrantPrincipal | None:
10266        kind = self._match_texts(("ROLE", "GROUP")) and self._prev.text.upper()
10267        principal = self._parse_id_var()
10268
10269        if not principal:
10270            return None
10271
10272        return self.expression(exp.GrantPrincipal(this=principal, kind=kind))
10273
10274    def _parse_grant_revoke_common(
10275        self,
10276    ) -> tuple[list | None, str | None, exp.Expr | None]:
10277        privileges = self._parse_csv(self._parse_grant_privilege)
10278
10279        self._match(TokenType.ON)
10280        kind = self._prev.text.upper() if self._match_set(self.CREATABLES) else None
10281
10282        # Attempt to parse the securable e.g. MySQL allows names
10283        # such as "foo.*", "*.*" which are not easily parseable yet
10284        securable = self._try_parse(self._parse_table_parts)
10285
10286        return privileges, kind, securable
10287
10288    def _parse_grant(self) -> exp.Grant | exp.Command:
10289        start = self._prev
10290
10291        privileges, kind, securable = self._parse_grant_revoke_common()
10292
10293        if not securable or not self._match_text_seq("TO"):
10294            return self._parse_as_command(start)
10295
10296        principals = self._parse_csv(self._parse_grant_principal)
10297
10298        grant_option = self._match_text_seq("WITH", "GRANT", "OPTION")
10299
10300        if self._curr:
10301            return self._parse_as_command(start)
10302
10303        return self.expression(
10304            exp.Grant(
10305                privileges=privileges,
10306                kind=kind,
10307                securable=securable,
10308                principals=principals,
10309                grant_option=grant_option,
10310            )
10311        )
10312
10313    def _parse_revoke(self) -> exp.Revoke | exp.Command:
10314        start = self._prev
10315
10316        grant_option = self._match_text_seq("GRANT", "OPTION", "FOR")
10317
10318        privileges, kind, securable = self._parse_grant_revoke_common()
10319
10320        if not securable or not self._match_text_seq("FROM"):
10321            return self._parse_as_command(start)
10322
10323        principals = self._parse_csv(self._parse_grant_principal)
10324
10325        cascade = None
10326        if self._match_texts(("CASCADE", "RESTRICT")):
10327            cascade = self._prev.text.upper()
10328
10329        if self._curr:
10330            return self._parse_as_command(start)
10331
10332        return self.expression(
10333            exp.Revoke(
10334                privileges=privileges,
10335                kind=kind,
10336                securable=securable,
10337                principals=principals,
10338                grant_option=grant_option,
10339                cascade=cascade,
10340            )
10341        )
10342
10343    def _parse_overlay(self) -> exp.Overlay:
10344        def _parse_overlay_arg(text: str) -> exp.Expr | None:
10345            return (
10346                self._parse_bitwise()
10347                if self._match(TokenType.COMMA) or self._match_text_seq(text)
10348                else None
10349            )
10350
10351        return self.expression(
10352            exp.Overlay(
10353                this=self._parse_bitwise(),
10354                expression=_parse_overlay_arg("PLACING"),
10355                from_=_parse_overlay_arg("FROM"),
10356                for_=_parse_overlay_arg("FOR"),
10357            )
10358        )
10359
10360    def _parse_format_name(self) -> exp.Property:
10361        # Note: Although not specified in the docs, Snowflake does accept a string/identifier
10362        # for FILE_FORMAT = <format_name>
10363        return self.expression(
10364            exp.Property(
10365                this=exp.var("FORMAT_NAME"), value=self._parse_string() or self._parse_table_parts()
10366            )
10367        )
10368
10369    def _parse_distinct_arg_function(self, func: type[F], distinct_index: int = 0) -> F:
10370        is_distinct = self._match(TokenType.DISTINCT)
10371        if not is_distinct:
10372            self._match(TokenType.ALL)
10373
10374        args = [self._parse_lambda()]
10375        if self._match(TokenType.COMMA):
10376            args.extend(self._parse_function_args())
10377
10378        target = seq_get(args, distinct_index)
10379        if is_distinct and target:
10380            args[distinct_index] = self.expression(exp.Distinct(expressions=[target]))
10381
10382        return func.from_arg_list(args)
10383
10384    def _identifier_expression(
10385        self, token: Token | None = None, quoted: bool | None = None
10386    ) -> exp.Identifier:
10387        token = token or self._prev
10388        return self.expression(exp.Identifier(this=token.text, quoted=quoted), token)
10389
10390    def _build_pipe_cte(
10391        self,
10392        query: exp.Query,
10393        expressions: list[exp.Expr],
10394        alias_cte: exp.TableAlias | None = None,
10395    ) -> exp.Select:
10396        new_cte: str | exp.TableAlias | None
10397        if alias_cte:
10398            new_cte = alias_cte
10399        else:
10400            self._pipe_cte_counter += 1
10401            new_cte = f"__tmp{self._pipe_cte_counter}"
10402
10403        with_ = query.args.get("with_")
10404        ctes = with_.pop() if with_ else None
10405
10406        new_select = exp.select(*expressions, copy=False).from_(new_cte, copy=False)
10407        if ctes:
10408            new_select.set("with_", ctes)
10409
10410        return new_select.with_(new_cte, as_=query, copy=False)
10411
10412    def _parse_pipe_syntax_select(self, query: exp.Select) -> exp.Select:
10413        select = self._parse_select(consume_pipe=False)
10414        if not select:
10415            return query
10416
10417        return self._build_pipe_cte(
10418            query=query.select(*select.expressions, append=False), expressions=[exp.Star()]
10419        )
10420
10421    def _parse_pipe_syntax_limit(self, query: exp.Select) -> exp.Select:
10422        limit = self._parse_limit()
10423        offset = self._parse_offset()
10424        if isinstance(limit, exp.Limit):
10425            curr_limit = query.args.get("limit", limit)
10426            curr_is_limit_all = isinstance(curr_limit, exp.Limit) and curr_limit.is_limit_all
10427
10428            # LIMIT ALL means "no cap", so it's never tighter than a numeric limit.
10429            if limit.is_limit_all:
10430                should_replace = curr_is_limit_all
10431            else:
10432                should_replace = (
10433                    curr_is_limit_all or curr_limit.expression.to_py() >= limit.expression.to_py()
10434                )
10435
10436            if should_replace:
10437                query.limit(limit, copy=False)
10438        if offset:
10439            curr_offset = query.args.get("offset")
10440            curr_offset = curr_offset.expression.to_py() if curr_offset else 0
10441            query.offset(exp.Literal.number(curr_offset + offset.expression.to_py()), copy=False)
10442
10443        return query
10444
10445    def _parse_pipe_syntax_aggregate_fields(self) -> exp.Expr | None:
10446        this = self._parse_disjunction()
10447        if self._match_text_seq("GROUP", "AND", advance=False):
10448            return this
10449
10450        this = self._parse_alias(this)
10451
10452        if self._match_set((TokenType.ASC, TokenType.DESC), advance=False):
10453            return self._parse_ordered(lambda: this)
10454
10455        return this
10456
10457    def _parse_pipe_syntax_aggregate_group_order_by(
10458        self, query: exp.Select, group_by_exists: bool = True
10459    ) -> exp.Select:
10460        expr = self._parse_csv(self._parse_pipe_syntax_aggregate_fields)
10461        aggregates_or_groups, orders = [], []
10462        for element in expr:
10463            if isinstance(element, exp.Ordered):
10464                this = element.this
10465                if isinstance(this, exp.Alias):
10466                    element.set("this", this.args["alias"])
10467                orders.append(element)
10468            else:
10469                this = element
10470            aggregates_or_groups.append(this)
10471
10472        if group_by_exists:
10473            query.select(
10474                *aggregates_or_groups, *query.expressions, append=False, copy=False
10475            ).group_by(
10476                *[projection.args.get("alias", projection) for projection in aggregates_or_groups],
10477                copy=False,
10478            )
10479        else:
10480            query.select(*aggregates_or_groups, append=False, copy=False)
10481
10482        if orders:
10483            return query.order_by(*orders, append=False, copy=False)
10484
10485        return query
10486
10487    def _parse_pipe_syntax_aggregate(self, query: exp.Select) -> exp.Select:
10488        self._match_text_seq("AGGREGATE")
10489        query = self._parse_pipe_syntax_aggregate_group_order_by(query, group_by_exists=False)
10490
10491        if self._match(TokenType.GROUP_BY) or (
10492            self._match_text_seq("GROUP", "AND") and self._match(TokenType.ORDER_BY)
10493        ):
10494            query = self._parse_pipe_syntax_aggregate_group_order_by(query)
10495
10496        return self._build_pipe_cte(query=query, expressions=[exp.Star()])
10497
10498    def _parse_pipe_syntax_set_operator(self, query: exp.Query) -> exp.Query | None:
10499        first_setop = self.parse_set_operation(this=query)
10500        if not first_setop:
10501            return None
10502
10503        def _parse_and_unwrap_query() -> exp.Expr | None:
10504            expr = self._parse_paren()
10505            return expr.assert_is(exp.Subquery).unnest() if expr else None
10506
10507        first_setop.this.pop()
10508
10509        setops = [
10510            first_setop.expression.pop().assert_is(exp.Subquery).unnest(),
10511            *self._parse_csv(_parse_and_unwrap_query),
10512        ]
10513
10514        query = self._build_pipe_cte(query=query, expressions=[exp.Star()])
10515        with_ = query.args.get("with_")
10516        ctes = with_.pop() if with_ else None
10517
10518        if isinstance(first_setop, exp.Union):
10519            query = query.union(*setops, copy=False, **first_setop.args)
10520        elif isinstance(first_setop, exp.Except):
10521            query = query.except_(*setops, copy=False, **first_setop.args)
10522        else:
10523            query = query.intersect(*setops, copy=False, **first_setop.args)
10524
10525        query.set("with_", ctes)
10526
10527        return self._build_pipe_cte(query=query, expressions=[exp.Star()])
10528
10529    def _parse_pipe_syntax_join(self, query: exp.Query) -> exp.Query | None:
10530        join = self._parse_join()
10531        if not join:
10532            return None
10533
10534        if isinstance(query, exp.Select):
10535            return query.join(join, copy=False)
10536
10537        return query
10538
10539    def _parse_pipe_syntax_pivot(self, query: exp.Select) -> exp.Select:
10540        pivots = self._parse_pivots()
10541        if not pivots:
10542            return query
10543
10544        from_ = query.args.get("from_")
10545        if from_:
10546            from_.this.set("pivots", pivots)
10547
10548        return self._build_pipe_cte(query=query, expressions=[exp.Star()])
10549
10550    def _parse_pipe_syntax_extend(self, query: exp.Select) -> exp.Select:
10551        self._match_text_seq("EXTEND")
10552        query.select(*[exp.Star(), *self._parse_expressions()], append=False, copy=False)
10553        return self._build_pipe_cte(query=query, expressions=[exp.Star()])
10554
10555    def _parse_pipe_syntax_tablesample(self, query: exp.Select) -> exp.Select:
10556        sample = self._parse_table_sample()
10557
10558        with_ = query.args.get("with_")
10559        if with_:
10560            with_.expressions[-1].this.set("sample", sample)
10561        else:
10562            query.set("sample", sample)
10563
10564        return query
10565
10566    def _parse_pipe_syntax_query(self, query: exp.Query) -> exp.Query | None:
10567        if isinstance(query, exp.Subquery):
10568            query = exp.select("*").from_(query, copy=False)
10569
10570        if not query.args.get("from_"):
10571            query = exp.select("*").from_(query.subquery(copy=False), copy=False)
10572
10573        while self._match(TokenType.PIPE_GT):
10574            start_index = self._index
10575            start_text = self._curr.text.upper()
10576            parser = self.PIPE_SYNTAX_TRANSFORM_PARSERS.get(start_text)
10577            if not parser:
10578                # The set operators (UNION, etc) and the JOIN operator have a few common starting
10579                # keywords, making it tricky to disambiguate them without lookahead. The approach
10580                # here is to try and parse a set operation and if that fails, then try to parse a
10581                # join operator. If that fails as well, then the operator is not supported.
10582                parsed_query = self._parse_pipe_syntax_set_operator(query)
10583                parsed_query = parsed_query or self._parse_pipe_syntax_join(query)
10584                if not parsed_query:
10585                    self._retreat(start_index)
10586                    self.raise_error(f"Unsupported pipe syntax operator: '{start_text}'.")
10587                    break
10588                query = parsed_query
10589            else:
10590                query = parser(self, query)
10591
10592        return query
10593
10594    def _parse_declareitem(self) -> exp.DeclareItem | None:
10595        self._match_texts(("VAR", "VARIABLE"))
10596
10597        vars = self._parse_csv(self._parse_id_var)
10598        if not vars:
10599            return None
10600
10601        self._match(TokenType.ALIAS)
10602        kind = self._parse_schema() if self._match(TokenType.TABLE) else self._parse_types()
10603        default = (
10604            self._match(TokenType.DEFAULT) or self._match(TokenType.EQ)
10605        ) and self._parse_bitwise()
10606
10607        return self.expression(exp.DeclareItem(this=vars, kind=kind, default=default))
10608
10609    def _parse_declare(self) -> exp.Declare | exp.Command:
10610        start = self._prev
10611        replace = self._match_text_seq("OR", "REPLACE")
10612        expressions = self._try_parse(lambda: self._parse_csv(self._parse_declareitem))
10613
10614        if not expressions or self._curr:
10615            return self._parse_as_command(start)
10616
10617        return self.expression(exp.Declare(expressions=expressions, replace=replace))
10618
10619    def build_cast(self, strict: bool, **kwargs) -> exp.Expr:
10620        exp_class = exp.Cast if strict else exp.TryCast
10621
10622        if exp_class == exp.TryCast:
10623            kwargs["requires_string"] = self.dialect.TRY_CAST_REQUIRES_STRING
10624
10625        return self.expression(exp_class(**kwargs))
10626
10627    def _parse_json_value(self) -> exp.JSONValue:
10628        this = self._parse_bitwise()
10629        self._match(TokenType.COMMA)
10630        path = self._parse_bitwise()
10631
10632        returning = self._match(TokenType.RETURNING) and self._parse_type()
10633
10634        return self.expression(
10635            exp.JSONValue(
10636                this=this,
10637                path=self.dialect.to_json_path(path),
10638                returning=returning,
10639                on_condition=self._parse_on_condition(),
10640            )
10641        )
10642
10643    def _parse_group_concat(self) -> exp.Expr | None:
10644        def concat_exprs(node: exp.Expr | None, exprs: list[exp.Expr]) -> exp.Expr:
10645            if isinstance(node, exp.Distinct) and len(node.expressions) > 1:
10646                concat_exprs = [
10647                    self.expression(
10648                        exp.Concat(
10649                            expressions=node.expressions,
10650                            safe=True,
10651                            coalesce=self.dialect.CONCAT_COALESCE,
10652                        )
10653                    )
10654                ]
10655                node.set("expressions", concat_exprs)
10656                return node
10657            if len(exprs) == 1:
10658                return exprs[0]
10659            return self.expression(
10660                exp.Concat(expressions=args, safe=True, coalesce=self.dialect.CONCAT_COALESCE)
10661            )
10662
10663        args = self._parse_csv(self._parse_lambda)
10664
10665        if args:
10666            order = args[-1] if isinstance(args[-1], exp.Order) else None
10667
10668            if order:
10669                # Order By is the last (or only) expression in the list and has consumed the 'expr' before it,
10670                # remove 'expr' from exp.Order and add it back to args
10671                args[-1] = order.this
10672                order.set("this", concat_exprs(order.this, args))
10673
10674            this = order or concat_exprs(args[0], args)
10675        else:
10676            this = None
10677
10678        separator = self._parse_field() if self._match(TokenType.SEPARATOR) else None
10679
10680        return self.expression(exp.GroupConcat(this=this, separator=separator))
10681
10682    def _parse_initcap(self) -> exp.Initcap:
10683        expr = exp.Initcap.from_arg_list(self._parse_function_args())
10684
10685        # attach dialect's default delimiters
10686        if expr.args.get("expression") is None:
10687            expr.set("expression", exp.Literal.string(self.dialect.INITCAP_DEFAULT_DELIMITER_CHARS))
10688
10689        return expr
10690
10691    def _parse_operator(self, this: exp.Expr | None) -> exp.Expr | None:
10692        if not self._match(TokenType.L_PAREN):
10693            self._retreat(self._index - 1)
10694            return None
10695
10696        op = ""
10697        while self._curr and not self._match(TokenType.R_PAREN):
10698            op += self._curr.text
10699            self._advance()
10700
10701        comments = self._prev_comments
10702        return self.expression(
10703            exp.Operator(this=this, operator=op, expression=self._parse_bitwise()),
10704            comments=comments,
10705        )
logger = <Logger sqlglot (WARNING)>
OPTIONS_TYPE = dict[str, collections.abc.Sequence[typing.Union[collections.abc.Sequence[str], str]]]
TEXTS_TYPE = typing.Union[tuple[str, ...], list[str], typing.AbstractSet[str], typing.Mapping[str, typing.Any]]
TIME_ZONE_RE: re.Pattern[str] = re.compile(':.*?[a-zA-Z\\+\\-]')
def build_var_map( args: Sequence[typing.Any]) -> sqlglot.expressions.array.StarMap | sqlglot.expressions.array.VarMap:
51def build_var_map(args: BuilderArgs) -> exp.StarMap | exp.VarMap:
52    if len(args) == 1 and args[0].is_star:
53        return exp.StarMap(this=args[0])
54
55    keys: list[ExpOrStr] = []
56    values: list[ExpOrStr] = []
57    for i in range(0, len(args), 2):
58        keys.append(args[i])
59        values.append(args[i + 1])
60
61    return exp.VarMap(keys=exp.array(*keys, copy=False), values=exp.array(*values, copy=False))
def build_like( args: Sequence[typing.Any]) -> sqlglot.expressions.core.Escape | sqlglot.expressions.core.Like:
64def build_like(args: BuilderArgs) -> exp.Escape | exp.Like:
65    like = exp.Like(this=seq_get(args, 1), expression=seq_get(args, 0))
66    return exp.Escape(this=like, expression=seq_get(args, 2)) if len(args) > 2 else like
def binary_range_parser( expr_type: type[sqlglot.expressions.core.Expr], reverse_args: bool = False) -> Callable[[Parser, sqlglot.expressions.core.Expr | None], sqlglot.expressions.core.Expr | None]:
69def binary_range_parser(
70    expr_type: Type[exp.Expr], reverse_args: bool = False
71) -> t.Callable[[Parser, exp.Expr | None], exp.Expr | None]:
72    def _parse_binary_range(self: Parser, this: exp.Expr | None) -> exp.Expr | None:
73        expression = self._parse_bitwise()
74        if reverse_args:
75            this, expression = expression, this
76        return self._parse_escape(self.expression(expr_type(this=this, expression=expression)))
77
78    return _parse_binary_range
def build_logarithm( args: Sequence[typing.Any], dialect: sqlglot.dialects.Dialect) -> sqlglot.expressions.core.Func:
81def build_logarithm(args: BuilderArgs, dialect: Dialect) -> exp.Func:
82    # Default argument order is base, expression
83    this = seq_get(args, 0)
84    expression = seq_get(args, 1)
85
86    if expression:
87        if not dialect.LOG_BASE_FIRST:
88            this, expression = expression, this
89        return exp.Log(this=this, expression=expression)
90
91    return (exp.Ln if dialect.parser_class.LOG_DEFAULTS_TO_LN else exp.Log)(this=this)
def build_hex( args: Sequence[typing.Any], dialect: sqlglot.dialects.Dialect) -> sqlglot.expressions.string.Hex | sqlglot.expressions.string.LowerHex:
94def build_hex(args: BuilderArgs, dialect: Dialect) -> exp.Hex | exp.LowerHex:
95    arg = seq_get(args, 0)
96    return exp.LowerHex(this=arg) if dialect.HEX_LOWERCASE else exp.Hex(this=arg)
def build_lower( args: Sequence[typing.Any]) -> sqlglot.expressions.string.Lower | sqlglot.expressions.string.Hex:
 99def build_lower(args: BuilderArgs) -> exp.Lower | exp.Hex:
100    # LOWER(HEX(..)) can be simplified to LowerHex to simplify its transpilation
101    arg = seq_get(args, 0)
102    return exp.LowerHex(this=arg.this) if isinstance(arg, exp.Hex) else exp.Lower(this=arg)
def build_upper( args: Sequence[typing.Any]) -> sqlglot.expressions.string.Upper | sqlglot.expressions.string.Hex:
105def build_upper(args: BuilderArgs) -> exp.Upper | exp.Hex:
106    # UPPER(HEX(..)) can be simplified to Hex to simplify its transpilation
107    arg = seq_get(args, 0)
108    return exp.Hex(this=arg.this) if isinstance(arg, exp.Hex) else exp.Upper(this=arg)
def build_extract_json_with_path( expr_type: type[~E]) -> Callable[[Sequence[Any], sqlglot.dialects.Dialect], ~E]:
111def build_extract_json_with_path(
112    expr_type: Type[E],
113) -> t.Callable[[BuilderArgs, Dialect], E]:
114    def _builder(args: BuilderArgs, dialect: Dialect) -> E:
115        expression = expr_type(
116            this=seq_get(args, 0), expression=dialect.to_json_path(seq_get(args, 1))
117        )
118        if len(args) > 2 and expr_type is exp.JSONExtract:
119            expression.set("expressions", args[2:])
120        if expr_type is exp.JSONExtractScalar:
121            expression.set("scalar_only", dialect.JSON_EXTRACT_SCALAR_SCALAR_ONLY)
122
123        return expression
124
125    return _builder
def build_mod(args: Sequence[typing.Any]) -> sqlglot.expressions.core.Mod:
128def build_mod(args: BuilderArgs) -> exp.Mod:
129    this = seq_get(args, 0)
130    expression = seq_get(args, 1)
131
132    # Wrap the operands if they are binary nodes, e.g. MOD(a + 1, 7) -> (a + 1) % 7
133    this = exp.Paren(this=this) if isinstance(this, exp.Binary) else this
134    expression = exp.Paren(this=expression) if isinstance(expression, exp.Binary) else expression
135
136    return exp.Mod(this=this, expression=expression)
def build_pad(args: Sequence[typing.Any], is_left: bool = True):
139def build_pad(args: BuilderArgs, is_left: bool = True):
140    return exp.Pad(
141        this=seq_get(args, 0),
142        expression=seq_get(args, 1),
143        fill_pattern=seq_get(args, 2),
144        is_left=is_left,
145    )
def build_array_constructor( exp_class: type[~E], args: list[typing.Any], bracket_kind: sqlglot.tokenizer_core.TokenType, dialect: sqlglot.dialects.Dialect) -> sqlglot.expressions.core.Expr:
148def build_array_constructor(
149    exp_class: Type[E], args: list[t.Any], bracket_kind: TokenType, dialect: Dialect
150) -> exp.Expr:
151    array_exp = exp_class(expressions=args)
152
153    if exp_class == exp.Array:
154        array_exp.set("value_constructor", len(args) == 1 and isinstance(args[0], exp.Query))
155        if dialect.HAS_DISTINCT_ARRAY_CONSTRUCTORS:
156            array_exp.set("bracket_notation", bracket_kind == TokenType.L_BRACKET)
157
158    return array_exp
def build_convert_timezone( args: Sequence[typing.Any], default_source_tz: str | None = None) -> sqlglot.expressions.temporal.ConvertTimezone | sqlglot.expressions.core.Anonymous:
161def build_convert_timezone(
162    args: BuilderArgs, default_source_tz: str | None = None
163) -> exp.ConvertTimezone | exp.Anonymous:
164    if len(args) == 2:
165        source_tz = exp.Literal.string(default_source_tz) if default_source_tz else None
166        return exp.ConvertTimezone(
167            source_tz=source_tz, target_tz=seq_get(args, 0), timestamp=seq_get(args, 1)
168        )
169
170    return exp.ConvertTimezone.from_arg_list(args)
def build_trim( args: Sequence[typing.Any], is_left: bool = True, reverse_args: bool = False) -> sqlglot.expressions.string.Trim:
173def build_trim(args: BuilderArgs, is_left: bool = True, reverse_args: bool = False) -> exp.Trim:
174    this, expression = seq_get(args, 0), seq_get(args, 1)
175
176    if expression and reverse_args:
177        this, expression = expression, this
178
179    return exp.Trim(this=this, expression=expression, position="LEADING" if is_left else "TRAILING")
def build_coalesce( args: Sequence[typing.Any], is_nvl: bool | None = None, is_null: bool | None = None) -> sqlglot.expressions.functions.Coalesce:
182def build_coalesce(
183    args: BuilderArgs, is_nvl: bool | None = None, is_null: bool | None = None
184) -> exp.Coalesce:
185    return exp.Coalesce(this=seq_get(args, 0), expressions=args[1:], is_nvl=is_nvl, is_null=is_null)
def build_locate_strposition(args: Sequence[typing.Any]) -> sqlglot.expressions.string.StrPosition:
188def build_locate_strposition(args: BuilderArgs) -> exp.StrPosition:
189    return exp.StrPosition(
190        this=seq_get(args, 1),
191        substr=seq_get(args, 0),
192        position=seq_get(args, 2),
193    )
def build_array_append( args: Sequence[typing.Any], dialect: sqlglot.dialects.Dialect) -> sqlglot.expressions.array.ArrayAppend:
196def build_array_append(args: BuilderArgs, dialect: Dialect) -> exp.ArrayAppend:
197    """
198    Builds ArrayAppend with NULL propagation semantics based on the dialect configuration.
199
200    Some dialects (Databricks, Spark, Snowflake) return NULL when the input array is NULL.
201    Others (DuckDB, PostgreSQL) create a new single-element array instead.
202
203    Args:
204        args: Function arguments [array, element]
205        dialect: The dialect to read ARRAY_FUNCS_PROPAGATES_NULLS from
206
207    Returns:
208        ArrayAppend expression with appropriate null_propagation flag
209    """
210    return exp.ArrayAppend(
211        this=seq_get(args, 0),
212        expression=seq_get(args, 1),
213        null_propagation=dialect.ARRAY_FUNCS_PROPAGATES_NULLS,
214    )

Builds ArrayAppend with NULL propagation semantics based on the dialect configuration.

Some dialects (Databricks, Spark, Snowflake) return NULL when the input array is NULL. Others (DuckDB, PostgreSQL) create a new single-element array instead.

Arguments:
  • args: Function arguments [array, element]
  • dialect: The dialect to read ARRAY_FUNCS_PROPAGATES_NULLS from
Returns:

ArrayAppend expression with appropriate null_propagation flag

def build_array_prepend( args: Sequence[typing.Any], dialect: sqlglot.dialects.Dialect) -> sqlglot.expressions.array.ArrayPrepend:
217def build_array_prepend(args: BuilderArgs, dialect: Dialect) -> exp.ArrayPrepend:
218    """
219    Builds ArrayPrepend with NULL propagation semantics based on the dialect configuration.
220
221    Some dialects (Databricks, Spark, Snowflake) return NULL when the input array is NULL.
222    Others (DuckDB, PostgreSQL) create a new single-element array instead.
223
224    Args:
225        args: Function arguments [array, element]
226        dialect: The dialect to read ARRAY_FUNCS_PROPAGATES_NULLS from
227
228    Returns:
229        ArrayPrepend expression with appropriate null_propagation flag
230    """
231    return exp.ArrayPrepend(
232        this=seq_get(args, 0),
233        expression=seq_get(args, 1),
234        null_propagation=dialect.ARRAY_FUNCS_PROPAGATES_NULLS,
235    )

Builds ArrayPrepend with NULL propagation semantics based on the dialect configuration.

Some dialects (Databricks, Spark, Snowflake) return NULL when the input array is NULL. Others (DuckDB, PostgreSQL) create a new single-element array instead.

Arguments:
  • args: Function arguments [array, element]
  • dialect: The dialect to read ARRAY_FUNCS_PROPAGATES_NULLS from
Returns:

ArrayPrepend expression with appropriate null_propagation flag

def build_array_concat( args: Sequence[typing.Any], dialect: sqlglot.dialects.Dialect) -> sqlglot.expressions.array.ArrayConcat:
238def build_array_concat(args: BuilderArgs, dialect: Dialect) -> exp.ArrayConcat:
239    """
240    Builds ArrayConcat with NULL propagation semantics based on the dialect configuration.
241
242    Some dialects (Redshift, Snowflake) return NULL when any input array is NULL.
243    Others (DuckDB, PostgreSQL) skip NULL arrays and continue concatenation.
244
245    Args:
246        args: Function arguments [array1, array2, ...] (variadic)
247        dialect: The dialect to read ARRAY_FUNCS_PROPAGATES_NULLS from
248
249    Returns:
250        ArrayConcat expression with appropriate null_propagation flag
251    """
252    return exp.ArrayConcat(
253        this=seq_get(args, 0),
254        expressions=args[1:],
255        null_propagation=dialect.ARRAY_FUNCS_PROPAGATES_NULLS,
256    )

Builds ArrayConcat with NULL propagation semantics based on the dialect configuration.

Some dialects (Redshift, Snowflake) return NULL when any input array is NULL. Others (DuckDB, PostgreSQL) skip NULL arrays and continue concatenation.

Arguments:
  • args: Function arguments [array1, array2, ...] (variadic)
  • dialect: The dialect to read ARRAY_FUNCS_PROPAGATES_NULLS from
Returns:

ArrayConcat expression with appropriate null_propagation flag

def build_array_remove( args: Sequence[typing.Any], dialect: sqlglot.dialects.Dialect) -> sqlglot.expressions.array.ArrayRemove:
259def build_array_remove(args: BuilderArgs, dialect: Dialect) -> exp.ArrayRemove:
260    """
261    Builds ArrayRemove with NULL propagation semantics based on the dialect configuration.
262
263    Some dialects (Snowflake) return NULL when the removal value is NULL.
264    Others (DuckDB) may return empty array due to NULL comparison semantics.
265
266    Args:
267        args: Function arguments [array, value_to_remove]
268        dialect: The dialect to read ARRAY_FUNCS_PROPAGATES_NULLS from
269
270    Returns:
271        ArrayRemove expression with appropriate null_propagation flag
272    """
273    return exp.ArrayRemove(
274        this=seq_get(args, 0),
275        expression=seq_get(args, 1),
276        null_propagation=dialect.ARRAY_FUNCS_PROPAGATES_NULLS,
277    )

Builds ArrayRemove with NULL propagation semantics based on the dialect configuration.

Some dialects (Snowflake) return NULL when the removal value is NULL. Others (DuckDB) may return empty array due to NULL comparison semantics.

Arguments:
  • args: Function arguments [array, value_to_remove]
  • dialect: The dialect to read ARRAY_FUNCS_PROPAGATES_NULLS from
Returns:

ArrayRemove expression with appropriate null_propagation flag

298def build_json_extract(self: Parser, this: exp.Expr, path: exp.Expr) -> exp.JSONExtract:
299    return self.expression(
300        exp.JSONExtract(
301            this=this,
302            expression=self.dialect.to_json_path(path),
303            only_json_types=self.JSON_ARROWS_REQUIRE_JSON_TYPE,
304        )
305    )
def build_json_extract_scalar( self: Parser, this: sqlglot.expressions.core.Expr, path: sqlglot.expressions.core.Expr) -> sqlglot.expressions.json.JSONExtractScalar:
308def build_json_extract_scalar(
309    self: Parser, this: exp.Expr, path: exp.Expr
310) -> exp.JSONExtractScalar:
311    return self.expression(
312        exp.JSONExtractScalar(
313            this=this,
314            expression=self.dialect.to_json_path(path),
315            only_json_types=self.JSON_ARROWS_REQUIRE_JSON_TYPE,
316            scalar_only=self.dialect.JSON_EXTRACT_SCALAR_SCALAR_ONLY,
317        )
318    )
321def build_jsonb_extract(self: Parser, this: exp.Expr, path: exp.Expr) -> exp.JSONBExtract:
322    return self.expression(exp.JSONBExtract(this=this, expression=path))
def build_jsonb_extract_scalar( self: Parser, this: sqlglot.expressions.core.Expr, path: sqlglot.expressions.core.Expr) -> sqlglot.expressions.json.JSONBExtractScalar:
325def build_jsonb_extract_scalar(
326    self: Parser, this: exp.Expr, path: exp.Expr
327) -> exp.JSONBExtractScalar:
328    return self.expression(exp.JSONBExtractScalar(this=this, expression=path))
def build_jsonb_contains_top_key( self: Parser, this: sqlglot.expressions.core.Expr, key: sqlglot.expressions.core.Expr) -> sqlglot.expressions.json.JSONBContainsTopKey:
331def build_jsonb_contains_top_key(
332    self: Parser, this: exp.Expr, key: exp.Expr
333) -> exp.JSONBContainsTopKey:
334    return self.expression(exp.JSONBContainsTopKey(this=this, expression=key))
SENTINEL_NONE: sqlglot.tokenizer_core.Token = <Token token_type: TokenType.SENTINEL, text: SENTINEL, line: 1, col: 1, start: 0, end: 0, comments: []>
class Parser:
  340class Parser:
  341    """
  342    Parser consumes a list of tokens produced by the Tokenizer and produces a parsed syntax tree.
  343
  344    Args:
  345        error_level: The desired error level.
  346            Default: ErrorLevel.IMMEDIATE
  347        error_message_context: The amount of context to capture from a query string when displaying
  348            the error message (in number of characters).
  349            Default: 100
  350        max_errors: Maximum number of error messages to include in a raised ParseError.
  351            This is only relevant if error_level is ErrorLevel.RAISE.
  352            Default: 3
  353        max_nodes: Maximum number of AST nodes to prevent memory exhaustion.
  354            Set to -1 (default) to disable the check.
  355    """
  356
  357    __slots__ = (
  358        "error_level",
  359        "error_message_context",
  360        "max_errors",
  361        "max_nodes",
  362        "dialect",
  363        "sql",
  364        "errors",
  365        "_tokens",
  366        "_index",
  367        "_curr",
  368        "_next",
  369        "_prev",
  370        "_prev_comments",
  371        "_pipe_cte_counter",
  372        "_chunks",
  373        "_chunk_index",
  374        "_tokens_size",
  375        "_node_count",
  376    )
  377
  378    FUNCTIONS: t.ClassVar[dict[str, t.Callable]] = {
  379        **{name: func.from_arg_list for name, func in exp.FUNCTION_BY_NAME.items()},
  380        **dict.fromkeys(("COALESCE", "IFNULL", "NVL"), build_coalesce),
  381        "ARRAY": lambda args, dialect: exp.Array(expressions=args),
  382        "ARRAYAGG": lambda args, dialect: exp.ArrayAgg(
  383            this=seq_get(args, 0), nulls_excluded=dialect.ARRAY_AGG_INCLUDES_NULLS is None or None
  384        ),
  385        "ARRAY_AGG": lambda args, dialect: exp.ArrayAgg(
  386            this=seq_get(args, 0), nulls_excluded=dialect.ARRAY_AGG_INCLUDES_NULLS is None or None
  387        ),
  388        "ARRAY_APPEND": build_array_append,
  389        "ARRAY_CAT": build_array_concat,
  390        "ARRAY_CONCAT": build_array_concat,
  391        "ARRAY_INTERSECT": lambda args: exp.ArrayIntersect(expressions=args),
  392        "ARRAY_INTERSECTION": lambda args: exp.ArrayIntersect(expressions=args),
  393        "ARRAY_PREPEND": build_array_prepend,
  394        "ARRAY_REMOVE": build_array_remove,
  395        "COUNT": lambda args: exp.Count(this=seq_get(args, 0), expressions=args[1:], big_int=True),
  396        "CONCAT": lambda args, dialect: exp.Concat(
  397            expressions=args,
  398            safe=not dialect.STRICT_STRING_CONCAT,
  399            coalesce=dialect.CONCAT_COALESCE,
  400        ),
  401        "CONCAT_WS": lambda args, dialect: exp.ConcatWs(
  402            expressions=args,
  403            safe=not dialect.STRICT_STRING_CONCAT,
  404            coalesce=dialect.CONCAT_WS_COALESCE,
  405        ),
  406        "CONVERT_TIMEZONE": build_convert_timezone,
  407        "DATE_TO_DATE_STR": lambda args: exp.Cast(
  408            this=seq_get(args, 0),
  409            to=exp.DataType(this=exp.DType.TEXT),
  410        ),
  411        "GENERATE_DATE_ARRAY": lambda args: exp.GenerateDateArray(
  412            start=seq_get(args, 0),
  413            end=seq_get(args, 1),
  414            step=seq_get(args, 2) or exp.Interval(this=exp.Literal.string(1), unit=exp.var("DAY")),
  415        ),
  416        "GENERATE_UUID": lambda args, dialect: exp.Uuid(
  417            is_string=dialect.UUID_IS_STRING_TYPE or None
  418        ),
  419        "GLOB": lambda args: exp.Glob(this=seq_get(args, 1), expression=seq_get(args, 0)),
  420        "GREATEST": lambda args, dialect: exp.Greatest(
  421            this=seq_get(args, 0),
  422            expressions=args[1:],
  423            ignore_nulls=dialect.LEAST_GREATEST_IGNORES_NULLS,
  424        ),
  425        "LEAST": lambda args, dialect: exp.Least(
  426            this=seq_get(args, 0),
  427            expressions=args[1:],
  428            ignore_nulls=dialect.LEAST_GREATEST_IGNORES_NULLS,
  429        ),
  430        "HEX": build_hex,
  431        "JSON_EXTRACT": build_extract_json_with_path(exp.JSONExtract),
  432        "JSON_EXTRACT_SCALAR": build_extract_json_with_path(exp.JSONExtractScalar),
  433        "JSON_EXTRACT_PATH_TEXT": build_extract_json_with_path(exp.JSONExtractScalar),
  434        "JSON_KEYS": lambda args, dialect: exp.JSONKeys(
  435            this=seq_get(args, 0), expression=dialect.to_json_path(seq_get(args, 1))
  436        ),
  437        "LIKE": build_like,
  438        "LOG": build_logarithm,
  439        "LOG2": lambda args: exp.Log(this=exp.Literal.number(2), expression=seq_get(args, 0)),
  440        "LOG10": lambda args: exp.Log(this=exp.Literal.number(10), expression=seq_get(args, 0)),
  441        "LOWER": build_lower,
  442        "LPAD": lambda args: build_pad(args),
  443        "LEFTPAD": lambda args: build_pad(args),
  444        "LTRIM": lambda args: build_trim(args),
  445        "MOD": build_mod,
  446        "RIGHTPAD": lambda args: build_pad(args, is_left=False),
  447        "RPAD": lambda args: build_pad(args, is_left=False),
  448        "RTRIM": lambda args: build_trim(args, is_left=False),
  449        "SCOPE_RESOLUTION": lambda args: (
  450            exp.ScopeResolution(expression=seq_get(args, 0))
  451            if len(args) != 2
  452            else exp.ScopeResolution(this=seq_get(args, 0), expression=seq_get(args, 1))
  453        ),
  454        "STRPOS": exp.StrPosition.from_arg_list,
  455        "CHARINDEX": lambda args: build_locate_strposition(args),
  456        "INSTR": exp.StrPosition.from_arg_list,
  457        "LOCATE": lambda args: build_locate_strposition(args),
  458        "TIME_TO_TIME_STR": lambda args: exp.Cast(
  459            this=seq_get(args, 0),
  460            to=exp.DataType(this=exp.DType.TEXT),
  461        ),
  462        "TO_HEX": build_hex,
  463        "TS_OR_DS_TO_DATE_STR": lambda args: exp.Substring(
  464            this=exp.Cast(
  465                this=seq_get(args, 0),
  466                to=exp.DataType(this=exp.DType.TEXT),
  467            ),
  468            start=exp.Literal.number(1),
  469            length=exp.Literal.number(10),
  470        ),
  471        "UNNEST": lambda args: exp.Unnest(expressions=ensure_list(seq_get(args, 0))),
  472        "UPPER": build_upper,
  473        "UUID": lambda args, dialect: exp.Uuid(is_string=dialect.UUID_IS_STRING_TYPE or None),
  474        "UUID_STRING": lambda args, dialect: exp.Uuid(
  475            this=seq_get(args, 0),
  476            name=seq_get(args, 1),
  477            is_string=dialect.UUID_IS_STRING_TYPE or None,
  478        ),
  479        "VAR_MAP": build_var_map,
  480    }
  481
  482    NO_PAREN_FUNCTIONS: t.ClassVar[dict] = {
  483        TokenType.CURRENT_DATE: exp.CurrentDate,
  484        TokenType.CURRENT_DATETIME: exp.CurrentDate,
  485        TokenType.CURRENT_TIME: exp.CurrentTime,
  486        TokenType.CURRENT_TIMESTAMP: exp.CurrentTimestamp,
  487        TokenType.CURRENT_USER: exp.CurrentUser,
  488        TokenType.CURRENT_ROLE: exp.CurrentRole,
  489    }
  490
  491    STRUCT_TYPE_TOKENS: t.ClassVar = {
  492        TokenType.NESTED,
  493        TokenType.OBJECT,
  494        TokenType.STRUCT,
  495        TokenType.UNION,
  496    }
  497
  498    NESTED_TYPE_TOKENS: t.ClassVar = {
  499        TokenType.ARRAY,
  500        TokenType.LIST,
  501        TokenType.LOWCARDINALITY,
  502        TokenType.MAP,
  503        TokenType.NULLABLE,
  504        TokenType.RANGE,
  505        *STRUCT_TYPE_TOKENS,
  506    }
  507
  508    ENUM_TYPE_TOKENS: t.ClassVar = {
  509        TokenType.DYNAMIC,
  510        TokenType.ENUM,
  511        TokenType.ENUM8,
  512        TokenType.ENUM16,
  513    }
  514
  515    AGGREGATE_TYPE_TOKENS: t.ClassVar = {
  516        TokenType.AGGREGATEFUNCTION,
  517        TokenType.SIMPLEAGGREGATEFUNCTION,
  518    }
  519
  520    TYPE_TOKENS: t.ClassVar = {
  521        TokenType.BIT,
  522        TokenType.BOOLEAN,
  523        TokenType.TINYINT,
  524        TokenType.UTINYINT,
  525        TokenType.SMALLINT,
  526        TokenType.USMALLINT,
  527        TokenType.INT,
  528        TokenType.UINT,
  529        TokenType.BIGINT,
  530        TokenType.UBIGINT,
  531        TokenType.BIGNUM,
  532        TokenType.INT128,
  533        TokenType.UINT128,
  534        TokenType.INT256,
  535        TokenType.UINT256,
  536        TokenType.MEDIUMINT,
  537        TokenType.UMEDIUMINT,
  538        TokenType.FIXEDSTRING,
  539        TokenType.FLOAT,
  540        TokenType.DOUBLE,
  541        TokenType.UDOUBLE,
  542        TokenType.CHAR,
  543        TokenType.NCHAR,
  544        TokenType.VARCHAR,
  545        TokenType.NVARCHAR,
  546        TokenType.BPCHAR,
  547        TokenType.TEXT,
  548        TokenType.MEDIUMTEXT,
  549        TokenType.LONGTEXT,
  550        TokenType.BLOB,
  551        TokenType.MEDIUMBLOB,
  552        TokenType.LONGBLOB,
  553        TokenType.BINARY,
  554        TokenType.VARBINARY,
  555        TokenType.JSON,
  556        TokenType.JSONB,
  557        TokenType.INTERVAL,
  558        TokenType.TINYBLOB,
  559        TokenType.TINYTEXT,
  560        TokenType.TIME,
  561        TokenType.TIMETZ,
  562        TokenType.TIME_NS,
  563        TokenType.TIMESTAMP,
  564        TokenType.TIMESTAMP_S,
  565        TokenType.TIMESTAMP_MS,
  566        TokenType.TIMESTAMP_NS,
  567        TokenType.TIMESTAMPTZ,
  568        TokenType.TIMESTAMPLTZ,
  569        TokenType.TIMESTAMPNTZ,
  570        TokenType.DATETIME,
  571        TokenType.DATETIME2,
  572        TokenType.DATETIME64,
  573        TokenType.SMALLDATETIME,
  574        TokenType.DATE,
  575        TokenType.DATE32,
  576        TokenType.INT4RANGE,
  577        TokenType.INT4MULTIRANGE,
  578        TokenType.INT8RANGE,
  579        TokenType.INT8MULTIRANGE,
  580        TokenType.NUMRANGE,
  581        TokenType.NUMMULTIRANGE,
  582        TokenType.TSRANGE,
  583        TokenType.TSMULTIRANGE,
  584        TokenType.TSTZRANGE,
  585        TokenType.TSTZMULTIRANGE,
  586        TokenType.DATERANGE,
  587        TokenType.DATEMULTIRANGE,
  588        TokenType.DECIMAL,
  589        TokenType.DECIMAL32,
  590        TokenType.DECIMAL64,
  591        TokenType.DECIMAL128,
  592        TokenType.DECIMAL256,
  593        TokenType.DECFLOAT,
  594        TokenType.UDECIMAL,
  595        TokenType.BIGDECIMAL,
  596        TokenType.UUID,
  597        TokenType.GEOGRAPHY,
  598        TokenType.GEOGRAPHYPOINT,
  599        TokenType.GEOMETRY,
  600        TokenType.POINT,
  601        TokenType.RING,
  602        TokenType.LINESTRING,
  603        TokenType.MULTILINESTRING,
  604        TokenType.POLYGON,
  605        TokenType.MULTIPOLYGON,
  606        TokenType.HLLSKETCH,
  607        TokenType.HSTORE,
  608        TokenType.PSEUDO_TYPE,
  609        TokenType.SUPER,
  610        TokenType.SERIAL,
  611        TokenType.SMALLSERIAL,
  612        TokenType.BIGSERIAL,
  613        TokenType.XML,
  614        TokenType.YEAR,
  615        TokenType.USERDEFINED,
  616        TokenType.MONEY,
  617        TokenType.SMALLMONEY,
  618        TokenType.ROWVERSION,
  619        TokenType.IMAGE,
  620        TokenType.VARIANT,
  621        TokenType.VECTOR,
  622        TokenType.VOID,
  623        TokenType.OBJECT,
  624        TokenType.OBJECT_IDENTIFIER,
  625        TokenType.INET,
  626        TokenType.IPADDRESS,
  627        TokenType.IPPREFIX,
  628        TokenType.IPV4,
  629        TokenType.IPV6,
  630        TokenType.UNKNOWN,
  631        TokenType.NOTHING,
  632        TokenType.NULL,
  633        TokenType.NAME,
  634        TokenType.TDIGEST,
  635        TokenType.DYNAMIC,
  636        *ENUM_TYPE_TOKENS,
  637        *NESTED_TYPE_TOKENS,
  638        *AGGREGATE_TYPE_TOKENS,
  639    }
  640
  641    SIGNED_TO_UNSIGNED_TYPE_TOKEN: t.ClassVar = {
  642        TokenType.BIGINT: TokenType.UBIGINT,
  643        TokenType.INT: TokenType.UINT,
  644        TokenType.MEDIUMINT: TokenType.UMEDIUMINT,
  645        TokenType.SMALLINT: TokenType.USMALLINT,
  646        TokenType.TINYINT: TokenType.UTINYINT,
  647        TokenType.DECIMAL: TokenType.UDECIMAL,
  648        TokenType.DOUBLE: TokenType.UDOUBLE,
  649    }
  650
  651    SUBQUERY_PREDICATES: t.ClassVar = {
  652        TokenType.ANY: exp.Any,
  653        TokenType.ALL: exp.All,
  654        TokenType.EXISTS: exp.Exists,
  655        TokenType.SOME: exp.Any,
  656    }
  657
  658    SUBQUERY_TOKENS: t.ClassVar = {
  659        TokenType.SELECT,
  660        TokenType.WITH,
  661        TokenType.FROM,
  662    }
  663
  664    RESERVED_TOKENS: t.ClassVar = {
  665        *Tokenizer.SINGLE_TOKENS.values(),
  666        TokenType.SELECT,
  667    } - {TokenType.IDENTIFIER}
  668
  669    # Tokens whose text is extracted from delimited source text (e.g. quoted identifiers,
  670    # string literals), so they must never be treated as keywords when matching by text
  671    TEXT_MATCH_EXCLUDED_TOKENS: t.ClassVar[frozenset] = frozenset(
  672        {
  673            TokenType.BIT_STRING,
  674            TokenType.BYTE_STRING,
  675            TokenType.HEREDOC_STRING,
  676            TokenType.HEX_STRING,
  677            TokenType.IDENTIFIER,
  678            TokenType.NATIONAL_STRING,
  679            TokenType.RAW_STRING,
  680            TokenType.STRING,
  681            TokenType.UNICODE_STRING,
  682        }
  683    )
  684
  685    DB_CREATABLES: t.ClassVar = {
  686        TokenType.DATABASE,
  687        TokenType.DICTIONARY,
  688        TokenType.FILE_FORMAT,
  689        TokenType.MODEL,
  690        TokenType.NAMESPACE,
  691        TokenType.SCHEMA,
  692        TokenType.SEMANTIC_VIEW,
  693        TokenType.SEQUENCE,
  694        TokenType.SINK,
  695        TokenType.SOURCE,
  696        TokenType.STAGE,
  697        TokenType.STORAGE_INTEGRATION,
  698        TokenType.STREAMLIT,
  699        TokenType.TABLE,
  700        TokenType.TAG,
  701        TokenType.VIEW,
  702        TokenType.WAREHOUSE,
  703    }
  704
  705    CREATABLES: t.ClassVar = {
  706        TokenType.COLUMN,
  707        TokenType.CONSTRAINT,
  708        TokenType.FOREIGN_KEY,
  709        TokenType.FUNCTION,
  710        TokenType.INDEX,
  711        TokenType.PROCEDURE,
  712        TokenType.TRIGGER,
  713        TokenType.TYPE,
  714        *DB_CREATABLES,
  715    }
  716
  717    TRIGGER_EVENTS: t.ClassVar = {
  718        TokenType.INSERT,
  719        TokenType.UPDATE,
  720        TokenType.DELETE,
  721        TokenType.TRUNCATE,
  722    }
  723
  724    ALTERABLES: t.ClassVar = {
  725        TokenType.INDEX,
  726        TokenType.TABLE,
  727        TokenType.VIEW,
  728        TokenType.SESSION,
  729    }
  730
  731    # Tokens that can represent identifiers
  732    ID_VAR_TOKENS: t.ClassVar[set] = {
  733        TokenType.ALL,
  734        TokenType.ANALYZE,
  735        TokenType.ATTACH,
  736        TokenType.VAR,
  737        TokenType.ANTI,
  738        TokenType.APPLY,
  739        TokenType.ASC,
  740        TokenType.ASOF,
  741        TokenType.AUTO_INCREMENT,
  742        TokenType.BEGIN,
  743        TokenType.BPCHAR,
  744        TokenType.CACHE,
  745        TokenType.CASE,
  746        TokenType.COLLATE,
  747        TokenType.COMMAND,
  748        TokenType.COMMENT,
  749        TokenType.COMMIT,
  750        TokenType.CONSTRAINT,
  751        TokenType.COPY,
  752        TokenType.CUBE,
  753        TokenType.CURRENT_SCHEMA,
  754        TokenType.DECLARE,
  755        TokenType.DEFAULT,
  756        TokenType.DELETE,
  757        TokenType.DESC,
  758        TokenType.DESCRIBE,
  759        TokenType.DETACH,
  760        TokenType.DICTIONARY,
  761        TokenType.DIV,
  762        TokenType.END,
  763        TokenType.EXECUTE,
  764        TokenType.EXPORT,
  765        TokenType.ESCAPE,
  766        TokenType.FALSE,
  767        TokenType.FIRST,
  768        TokenType.FILE,
  769        TokenType.FILTER,
  770        TokenType.FINAL,
  771        TokenType.FORMAT,
  772        TokenType.FULL,
  773        TokenType.GET,
  774        TokenType.IDENTIFIER,
  775        TokenType.INOUT,
  776        TokenType.IS,
  777        TokenType.ISNULL,
  778        TokenType.INTERVAL,
  779        TokenType.KEEP,
  780        TokenType.KILL,
  781        TokenType.LEFT,
  782        TokenType.LIMIT,
  783        TokenType.LOAD,
  784        TokenType.LOCK,
  785        TokenType.MATCH,
  786        TokenType.MERGE,
  787        TokenType.NATURAL,
  788        TokenType.NEXT,
  789        TokenType.OFFSET,
  790        TokenType.OPERATOR,
  791        TokenType.ORDINALITY,
  792        TokenType.OUT,
  793        TokenType.OVER,
  794        TokenType.OVERLAPS,
  795        TokenType.OVERWRITE,
  796        TokenType.PARTITION,
  797        TokenType.PERCENT,
  798        TokenType.PIVOT,
  799        TokenType.PROJECTION,
  800        TokenType.PRAGMA,
  801        TokenType.PUT,
  802        TokenType.RANGE,
  803        TokenType.RECURSIVE,
  804        TokenType.REFERENCES,
  805        TokenType.REFRESH,
  806        TokenType.RENAME,
  807        TokenType.REPLACE,
  808        TokenType.RIGHT,
  809        TokenType.ROLLUP,
  810        TokenType.ROW,
  811        TokenType.ROWS,
  812        TokenType.SEMI,
  813        TokenType.SET,
  814        TokenType.SETTINGS,
  815        TokenType.SHOW,
  816        TokenType.STREAM,
  817        TokenType.STREAMLIT,
  818        TokenType.TEMPORARY,
  819        TokenType.TOP,
  820        TokenType.TRUE,
  821        TokenType.TRUNCATE,
  822        TokenType.UNIQUE,
  823        TokenType.UNNEST,
  824        TokenType.UNPIVOT,
  825        TokenType.UPDATE,
  826        TokenType.USE,
  827        TokenType.VOLATILE,
  828        TokenType.WINDOW,
  829        TokenType.CURRENT_CATALOG,
  830        TokenType.LOCALTIME,
  831        TokenType.LOCALTIMESTAMP,
  832        TokenType.SESSION_USER,
  833        TokenType.STRAIGHT_JOIN,
  834        *ALTERABLES,
  835        *CREATABLES,
  836        *SUBQUERY_PREDICATES,
  837        *TYPE_TOKENS,
  838        *NO_PAREN_FUNCTIONS,
  839    } - {TokenType.UNION}
  840
  841    TABLE_ALIAS_TOKENS: t.ClassVar[set] = ID_VAR_TOKENS - {
  842        TokenType.ANTI,
  843        TokenType.ASOF,
  844        TokenType.FULL,
  845        TokenType.LEFT,
  846        TokenType.LOCK,
  847        TokenType.NATURAL,
  848        TokenType.RIGHT,
  849        TokenType.SEMI,
  850        TokenType.WINDOW,
  851    }
  852
  853    ALIAS_TOKENS: t.ClassVar = ID_VAR_TOKENS
  854
  855    COLON_PLACEHOLDER_TOKENS: t.ClassVar = ID_VAR_TOKENS
  856
  857    ARRAY_CONSTRUCTORS: t.ClassVar = {
  858        "ARRAY": exp.Array,
  859        "LIST": exp.List,
  860    }
  861
  862    COMMENT_TABLE_ALIAS_TOKENS: t.ClassVar = TABLE_ALIAS_TOKENS - {TokenType.IS}
  863
  864    UPDATE_ALIAS_TOKENS: t.ClassVar = TABLE_ALIAS_TOKENS - {TokenType.SET}
  865
  866    TRIM_TYPES: t.ClassVar = {"LEADING", "TRAILING", "BOTH"}
  867
  868    # Tokens that indicate a simple column reference
  869    IDENTIFIER_TOKENS: t.ClassVar[frozenset] = frozenset({TokenType.VAR, TokenType.IDENTIFIER})
  870
  871    BRACKETS: t.ClassVar[frozenset] = frozenset({TokenType.L_BRACKET, TokenType.L_BRACE})
  872
  873    # Postfix tokens that prevent the bare column fast path
  874    COLUMN_POSTFIX_TOKENS: t.ClassVar[frozenset] = frozenset(
  875        {
  876            TokenType.L_PAREN,
  877            TokenType.L_BRACKET,
  878            TokenType.L_BRACE,
  879            TokenType.COLON,
  880            TokenType.JOIN_MARKER,
  881        }
  882    )
  883
  884    TABLE_POSTFIX_TOKENS: t.ClassVar[frozenset] = frozenset(
  885        {
  886            TokenType.L_PAREN,
  887            TokenType.L_BRACKET,
  888            TokenType.L_BRACE,
  889            TokenType.PIVOT,
  890            TokenType.UNPIVOT,
  891            TokenType.TABLE_SAMPLE,
  892        }
  893    )
  894
  895    FUNC_TOKENS: t.ClassVar = {
  896        TokenType.COLLATE,
  897        TokenType.COMMAND,
  898        TokenType.CURRENT_DATE,
  899        TokenType.CURRENT_DATETIME,
  900        TokenType.CURRENT_SCHEMA,
  901        TokenType.CURRENT_TIMESTAMP,
  902        TokenType.CURRENT_TIME,
  903        TokenType.CURRENT_USER,
  904        TokenType.CURRENT_CATALOG,
  905        TokenType.DECLARE,
  906        TokenType.FILTER,
  907        TokenType.FIRST,
  908        TokenType.FORMAT,
  909        TokenType.GET,
  910        TokenType.GLOB,
  911        TokenType.IDENTIFIER,
  912        TokenType.INDEX,
  913        TokenType.ISNULL,
  914        TokenType.ILIKE,
  915        TokenType.INSERT,
  916        TokenType.LIKE,
  917        TokenType.LOCALTIME,
  918        TokenType.LOCALTIMESTAMP,
  919        TokenType.MERGE,
  920        TokenType.NEXT,
  921        TokenType.OFFSET,
  922        TokenType.PRIMARY_KEY,
  923        TokenType.RANGE,
  924        TokenType.REPLACE,
  925        TokenType.RLIKE,
  926        TokenType.ROW,
  927        TokenType.SESSION_USER,
  928        TokenType.UNNEST,
  929        TokenType.VAR,
  930        TokenType.LEFT,
  931        TokenType.RIGHT,
  932        TokenType.SEQUENCE,
  933        TokenType.DATE,
  934        TokenType.DATETIME,
  935        TokenType.TABLE,
  936        TokenType.TIMESTAMP,
  937        TokenType.TIMESTAMPTZ,
  938        TokenType.TRUNCATE,
  939        TokenType.UTC_DATE,
  940        TokenType.UTC_TIME,
  941        TokenType.UTC_TIMESTAMP,
  942        TokenType.WINDOW,
  943        TokenType.XOR,
  944        *TYPE_TOKENS,
  945        *SUBQUERY_PREDICATES,
  946    }
  947
  948    CONJUNCTION: t.ClassVar[dict[TokenType, type[exp.Expr]]] = {
  949        TokenType.AND: exp.And,
  950    }
  951
  952    ASSIGNMENT: t.ClassVar[dict[TokenType, type[exp.Expr]]] = {
  953        TokenType.COLON_EQ: exp.PropertyEQ,
  954    }
  955
  956    DISJUNCTION: t.ClassVar[dict[TokenType, type[exp.Expr]]] = {
  957        TokenType.OR: exp.Or,
  958    }
  959
  960    EQUALITY: t.ClassVar = {
  961        TokenType.EQ: exp.EQ,
  962        TokenType.NEQ: exp.NEQ,
  963        TokenType.NULLSAFE_EQ: exp.NullSafeEQ,
  964    }
  965
  966    COMPARISON: t.ClassVar = {
  967        TokenType.GT: exp.GT,
  968        TokenType.GTE: exp.GTE,
  969        TokenType.LT: exp.LT,
  970        TokenType.LTE: exp.LTE,
  971    }
  972
  973    BITWISE: t.ClassVar = {
  974        TokenType.AMP: exp.BitwiseAnd,
  975        TokenType.CARET: exp.BitwiseXor,
  976        TokenType.PIPE: exp.BitwiseOr,
  977    }
  978
  979    TERM: t.ClassVar = {
  980        TokenType.DASH: exp.Sub,
  981        TokenType.PLUS: exp.Add,
  982        TokenType.COLLATE: exp.Collate,
  983    }
  984
  985    FACTOR: t.ClassVar = {
  986        TokenType.DIV: exp.IntDiv,
  987        TokenType.LR_ARROW: exp.Distance,
  988        TokenType.LLRR_ARROW: exp.DistanceNd,
  989        TokenType.MOD: exp.Mod,
  990        TokenType.SLASH: exp.Div,
  991        TokenType.STAR: exp.Mul,
  992    }
  993
  994    EXPONENT: t.ClassVar[dict[TokenType, type[exp.Expr]]] = {}
  995
  996    TIMES: t.ClassVar = {
  997        TokenType.TIME,
  998        TokenType.TIMETZ,
  999    }
 1000
 1001    TIMESTAMPS: t.ClassVar = {
 1002        TokenType.TIMESTAMP,
 1003        TokenType.TIMESTAMPNTZ,
 1004        TokenType.TIMESTAMPTZ,
 1005        TokenType.TIMESTAMPLTZ,
 1006        *TIMES,
 1007    }
 1008
 1009    SET_OPERATIONS: t.ClassVar = {
 1010        TokenType.UNION,
 1011        TokenType.INTERSECT,
 1012        TokenType.EXCEPT,
 1013    }
 1014
 1015    JOIN_METHODS: t.ClassVar = {
 1016        TokenType.ASOF,
 1017        TokenType.NATURAL,
 1018        TokenType.POSITIONAL,
 1019    }
 1020
 1021    JOIN_SIDES: t.ClassVar = {
 1022        TokenType.LEFT,
 1023        TokenType.RIGHT,
 1024        TokenType.FULL,
 1025    }
 1026
 1027    JOIN_KINDS: t.ClassVar = {
 1028        TokenType.ANTI,
 1029        TokenType.CROSS,
 1030        TokenType.INNER,
 1031        TokenType.OUTER,
 1032        TokenType.SEMI,
 1033        TokenType.STRAIGHT_JOIN,
 1034    }
 1035
 1036    JOIN_HINTS: t.ClassVar[set[str]] = set()
 1037
 1038    # Tokens that unambiguously end a table reference on the fast path
 1039    TABLE_TERMINATORS: t.ClassVar[frozenset] = frozenset(
 1040        {
 1041            TokenType.COMMA,
 1042            TokenType.GROUP_BY,
 1043            TokenType.HAVING,
 1044            TokenType.JOIN,
 1045            TokenType.LIMIT,
 1046            TokenType.ON,
 1047            TokenType.ORDER_BY,
 1048            TokenType.R_PAREN,
 1049            TokenType.SEMICOLON,
 1050            TokenType.SENTINEL,
 1051            TokenType.WHERE,
 1052            *SET_OPERATIONS,
 1053            *JOIN_KINDS,
 1054            *JOIN_METHODS,
 1055            *JOIN_SIDES,
 1056        }
 1057    )
 1058
 1059    LAMBDAS: t.ClassVar = {
 1060        TokenType.ARROW: lambda self, expressions: self.expression(
 1061            exp.Lambda(
 1062                this=self._replace_lambda(
 1063                    self._parse_disjunction(),
 1064                    expressions,
 1065                ),
 1066                expressions=expressions,
 1067            )
 1068        ),
 1069        TokenType.FARROW: lambda self, expressions: self.expression(
 1070            exp.Kwarg(
 1071                this=exp.var(expressions[0].name),
 1072                expression=self._parse_disjunction() or self._parse_select(),
 1073            )
 1074        ),
 1075    }
 1076
 1077    # Whether lambda args include type annotations, e.g. TRANSFORM(arr, x INT -> x + 1) in Snowflake
 1078    TYPED_LAMBDA_ARGS: t.ClassVar[bool] = False
 1079
 1080    LAMBDA_ARG_TERMINATORS: t.ClassVar[frozenset] = frozenset({TokenType.COMMA, TokenType.R_PAREN})
 1081
 1082    COLUMN_OPERATORS: t.ClassVar = {
 1083        TokenType.DOT: None,
 1084        TokenType.DOTCOLON: lambda self, this, to: self.expression(exp.JSONCast(this=this, to=to)),
 1085        TokenType.DCOLON: lambda self, this, to: self.build_cast(
 1086            strict=self.STRICT_CAST, this=this, to=to
 1087        ),
 1088        TokenType.ARROW: lambda self, this, path: self.expression(
 1089            exp.JSONExtract(
 1090                this=this,
 1091                expression=self.dialect.to_json_path(path),
 1092                only_json_types=self.JSON_ARROWS_REQUIRE_JSON_TYPE,
 1093            )
 1094        ),
 1095        TokenType.DARROW: lambda self, this, path: self.expression(
 1096            exp.JSONExtractScalar(
 1097                this=this,
 1098                expression=self.dialect.to_json_path(path),
 1099                only_json_types=self.JSON_ARROWS_REQUIRE_JSON_TYPE,
 1100                scalar_only=self.dialect.JSON_EXTRACT_SCALAR_SCALAR_ONLY,
 1101            )
 1102        ),
 1103        TokenType.HASH_ARROW: lambda self, this, path: self.expression(
 1104            exp.JSONBExtract(this=this, expression=path)
 1105        ),
 1106        TokenType.DHASH_ARROW: lambda self, this, path: self.expression(
 1107            exp.JSONBExtractScalar(this=this, expression=path)
 1108        ),
 1109        TokenType.PLACEHOLDER: lambda self, this, key: self.expression(
 1110            exp.JSONBContainsTopKey(this=this, expression=key)
 1111        ),
 1112    }
 1113
 1114    # JSON/JSONB operators (extraction and containment) at Postgres's "any other operator"
 1115    # tier, below +/-, level with ||. Same value signature as COLUMN_OPERATORS: (self, this, rhs).
 1116    JSON_OPERATORS: t.ClassVar[dict[TokenType, t.Callable]] = {}
 1117
 1118    CAST_COLUMN_OPERATORS: t.ClassVar = {
 1119        TokenType.DOTCOLON,
 1120        TokenType.DCOLON,
 1121    }
 1122
 1123    EXPRESSION_PARSERS: t.ClassVar = {
 1124        exp.Cluster: lambda self: self._parse_sort(exp.Cluster, TokenType.CLUSTER_BY),
 1125        exp.Column: lambda self: self._parse_column(),
 1126        exp.ColumnDef: lambda self: self._parse_column_def(self._parse_column()),
 1127        exp.Condition: lambda self: self._parse_disjunction(),
 1128        exp.DataType: lambda self: self._parse_types(allow_identifiers=False, schema=True),
 1129        exp.Expr: lambda self: self._parse_expression(),
 1130        exp.From: lambda self: self._parse_from(joins=True),
 1131        exp.GrantPrincipal: lambda self: self._parse_grant_principal(),
 1132        exp.GrantPrivilege: lambda self: self._parse_grant_privilege(),
 1133        exp.Group: lambda self: self._parse_group(),
 1134        exp.Having: lambda self: self._parse_having(),
 1135        exp.Hint: lambda self: self._parse_hint_body(),
 1136        exp.Identifier: lambda self: self._parse_id_var(),
 1137        exp.Join: lambda self: self._parse_join(),
 1138        exp.Lambda: lambda self: self._parse_lambda(),
 1139        exp.Lateral: lambda self: self._parse_lateral(),
 1140        exp.Limit: lambda self: self._parse_limit(),
 1141        exp.Offset: lambda self: self._parse_offset(),
 1142        exp.Order: lambda self: self._parse_order(),
 1143        exp.Ordered: lambda self: self._parse_ordered(),
 1144        exp.Properties: lambda self: self._parse_properties(),
 1145        exp.PartitionedByProperty: lambda self: self._parse_partitioned_by(),
 1146        exp.Qualify: lambda self: self._parse_qualify(),
 1147        exp.Returning: lambda self: self._parse_returning(),
 1148        exp.Select: lambda self: self._parse_select(),
 1149        exp.Sort: lambda self: self._parse_sort(exp.Sort, TokenType.SORT_BY),
 1150        exp.Table: lambda self: self._parse_table_parts(),
 1151        exp.TableAlias: lambda self: self._parse_table_alias(),
 1152        exp.Tuple: lambda self: self._parse_value(values=False),
 1153        exp.Whens: lambda self: self._parse_when_matched(),
 1154        exp.Where: lambda self: self._parse_where(),
 1155        exp.Window: lambda self: self._parse_named_window(),
 1156        exp.With: lambda self: self._parse_with(),
 1157    }
 1158
 1159    STATEMENT_PARSERS: t.ClassVar = {
 1160        TokenType.ALTER: lambda self: self._parse_alter(),
 1161        TokenType.ANALYZE: lambda self: self._parse_analyze(),
 1162        TokenType.BEGIN: lambda self: self._parse_transaction(),
 1163        TokenType.CACHE: lambda self: self._parse_cache(),
 1164        TokenType.COMMENT: lambda self: self._parse_comment(),
 1165        TokenType.COMMIT: lambda self: self._parse_commit_or_rollback(),
 1166        TokenType.COPY: lambda self: self._parse_copy(),
 1167        TokenType.CREATE: lambda self: self._parse_create(),
 1168        TokenType.DECLARE: lambda self: self._parse_declare(),
 1169        TokenType.DELETE: lambda self: self._parse_delete(),
 1170        TokenType.DESC: lambda self: self._parse_describe(),
 1171        TokenType.DESCRIBE: lambda self: self._parse_describe(),
 1172        TokenType.DROP: lambda self: self._parse_drop(),
 1173        TokenType.GRANT: lambda self: self._parse_grant(),
 1174        TokenType.REVOKE: lambda self: self._parse_revoke(),
 1175        TokenType.INSERT: lambda self: self._parse_insert(),
 1176        TokenType.KILL: lambda self: self._parse_kill(),
 1177        TokenType.LOAD: lambda self: self._parse_load(),
 1178        TokenType.MERGE: lambda self: self._parse_merge(),
 1179        TokenType.PIVOT: lambda self: self._parse_simplified_pivot(),
 1180        TokenType.PRAGMA: lambda self: self.expression(exp.Pragma(this=self._parse_expression())),
 1181        TokenType.REFRESH: lambda self: self._parse_refresh(),
 1182        TokenType.ROLLBACK: lambda self: self._parse_commit_or_rollback(),
 1183        TokenType.SET: lambda self: self._parse_set(),
 1184        TokenType.TRUNCATE: lambda self: self._parse_truncate_table(),
 1185        TokenType.UNCACHE: lambda self: self._parse_uncache(),
 1186        TokenType.UNPIVOT: lambda self: self._parse_simplified_pivot(is_unpivot=True),
 1187        TokenType.UPDATE: lambda self: self._parse_update(),
 1188        TokenType.USE: lambda self: self._parse_use(),
 1189        TokenType.SEMICOLON: lambda self: exp.Semicolon(),
 1190    }
 1191
 1192    UNARY_PARSERS: t.ClassVar = {
 1193        TokenType.PLUS: lambda self: self._parse_unary(),  # Unary + is handled as a no-op
 1194        TokenType.NOT: lambda self: self.expression(exp.Not(this=self._parse_equality())),
 1195        TokenType.TILDE: lambda self: self.expression(exp.BitwiseNot(this=self._parse_unary())),
 1196        TokenType.DASH: lambda self: self.expression(exp.Neg(this=self._parse_unary())),
 1197        TokenType.PIPE_SLASH: lambda self: self.expression(exp.Sqrt(this=self._parse_unary())),
 1198        TokenType.DPIPE_SLASH: lambda self: self.expression(exp.Cbrt(this=self._parse_unary())),
 1199    }
 1200
 1201    STRING_PARSERS: t.ClassVar = {
 1202        TokenType.HEREDOC_STRING: lambda self, token: self.expression(
 1203            exp.RawString(this=token.text), token
 1204        ),
 1205        TokenType.NATIONAL_STRING: lambda self, token: self.expression(
 1206            exp.National(this=token.text), token
 1207        ),
 1208        TokenType.RAW_STRING: lambda self, token: self.expression(
 1209            exp.RawString(this=token.text), token
 1210        ),
 1211        TokenType.STRING: lambda self, token: self.expression(
 1212            exp.Literal(this=token.text, is_string=True), token
 1213        ),
 1214        TokenType.UNICODE_STRING: lambda self, token: self.expression(
 1215            exp.UnicodeString(
 1216                this=token.text, escape=self._match_text_seq("UESCAPE") and self._parse_string()
 1217            ),
 1218            token,
 1219        ),
 1220    }
 1221
 1222    NUMERIC_PARSERS: t.ClassVar = {
 1223        TokenType.BIT_STRING: lambda self, token: self.expression(
 1224            exp.BitString(this=token.text), token
 1225        ),
 1226        TokenType.BYTE_STRING: lambda self, token: self.expression(
 1227            exp.ByteString(
 1228                this=token.text, is_bytes=self.dialect.BYTE_STRING_IS_BYTES_TYPE or None
 1229            ),
 1230            token,
 1231        ),
 1232        TokenType.HEX_STRING: lambda self, token: self.expression(
 1233            exp.HexString(
 1234                this=token.text, is_integer=self.dialect.HEX_STRING_IS_INTEGER_TYPE or None
 1235            ),
 1236            token,
 1237        ),
 1238        TokenType.NUMBER: lambda self, token: self.expression(
 1239            exp.Literal(this=token.text, is_string=False), token
 1240        ),
 1241    }
 1242
 1243    PRIMARY_PARSERS: t.ClassVar = {
 1244        **STRING_PARSERS,
 1245        **NUMERIC_PARSERS,
 1246        TokenType.INTRODUCER: lambda self, token: self._parse_introducer(token),
 1247        TokenType.NULL: lambda self, _: self.expression(exp.Null()),
 1248        TokenType.TRUE: lambda self, _: self.expression(exp.Boolean(this=True)),
 1249        TokenType.FALSE: lambda self, _: self.expression(exp.Boolean(this=False)),
 1250        TokenType.SESSION_PARAMETER: lambda self, _: self._parse_session_parameter(),
 1251        TokenType.STAR: lambda self, _: self._parse_star_ops(),
 1252    }
 1253
 1254    PLACEHOLDER_PARSERS: t.ClassVar = {
 1255        TokenType.PLACEHOLDER: lambda self: self.expression(exp.Placeholder()),
 1256        TokenType.PARAMETER: lambda self: self._parse_parameter(),
 1257        TokenType.COLON: lambda self: (
 1258            self.expression(exp.Placeholder(this=self._prev.text))
 1259            if self._match_set(self.COLON_PLACEHOLDER_TOKENS)
 1260            else None
 1261        ),
 1262    }
 1263
 1264    RANGE_PARSERS: t.ClassVar = {
 1265        TokenType.AT_GT: binary_range_parser(exp.ArrayContainsAll),
 1266        TokenType.BETWEEN: lambda self, this: self._parse_between(this),
 1267        TokenType.GLOB: binary_range_parser(exp.Glob),
 1268        TokenType.ILIKE: binary_range_parser(exp.ILike),
 1269        TokenType.IN: lambda self, this: self._parse_in(this),
 1270        TokenType.IRLIKE: binary_range_parser(exp.RegexpILike),
 1271        TokenType.IS: lambda self, this: self._parse_is(this),
 1272        TokenType.LIKE: binary_range_parser(exp.Like),
 1273        TokenType.LT_AT: binary_range_parser(exp.ArrayContainedBy),
 1274        TokenType.OVERLAPS: binary_range_parser(exp.Overlaps),
 1275        TokenType.RLIKE: binary_range_parser(exp.RegexpLike),
 1276        TokenType.SIMILAR_TO: binary_range_parser(exp.SimilarTo),
 1277        TokenType.FOR: lambda self, this: self._parse_comprehension(this),
 1278        TokenType.QMARK_AMP: binary_range_parser(exp.JSONBContainsAllTopKeys),
 1279        TokenType.QMARK_PIPE: binary_range_parser(exp.JSONBContainsAnyTopKeys),
 1280        TokenType.HASH_DASH: binary_range_parser(exp.JSONBDeleteAtPath),
 1281        TokenType.AT_QMARK: binary_range_parser(exp.JSONBPathExists),
 1282        TokenType.ADJACENT: binary_range_parser(exp.Adjacent),
 1283        TokenType.OPERATOR: lambda self, this: self._parse_operator(this),
 1284        TokenType.AMP_LT: binary_range_parser(exp.ExtendsLeft),
 1285        TokenType.AMP_GT: binary_range_parser(exp.ExtendsRight),
 1286        TokenType.CARET_AT: binary_range_parser(exp.StartsWith),
 1287    }
 1288
 1289    PIPE_SYNTAX_TRANSFORM_PARSERS: t.ClassVar = {
 1290        "AGGREGATE": lambda self, query: self._parse_pipe_syntax_aggregate(query),
 1291        "AS": lambda self, query: self._build_pipe_cte(
 1292            query, [exp.Star()], self._parse_table_alias()
 1293        ),
 1294        "DISTINCT": lambda self, query: self._advance() or query.distinct(copy=False),
 1295        "EXTEND": lambda self, query: self._parse_pipe_syntax_extend(query),
 1296        "LIMIT": lambda self, query: self._parse_pipe_syntax_limit(query),
 1297        "ORDER BY": lambda self, query: query.order_by(
 1298            self._parse_order(), append=False, copy=False
 1299        ),
 1300        "PIVOT": lambda self, query: self._parse_pipe_syntax_pivot(query),
 1301        "SELECT": lambda self, query: self._parse_pipe_syntax_select(query),
 1302        "TABLESAMPLE": lambda self, query: self._parse_pipe_syntax_tablesample(query),
 1303        "UNPIVOT": lambda self, query: self._parse_pipe_syntax_pivot(query),
 1304        "WHERE": lambda self, query: query.where(self._parse_where(), copy=False),
 1305    }
 1306
 1307    PROPERTY_PARSERS: t.ClassVar[dict[str, t.Callable]] = {
 1308        "ALLOWED_VALUES": lambda self: self.expression(
 1309            exp.AllowedValuesProperty(expressions=self._parse_csv(self._parse_primary))
 1310        ),
 1311        "ALGORITHM": lambda self: self._parse_property_assignment(exp.AlgorithmProperty),
 1312        "AUTO": lambda self: self._parse_auto_property(),
 1313        "AUTO_INCREMENT": lambda self: self._parse_property_assignment(exp.AutoIncrementProperty),
 1314        "BACKUP": lambda self: self.expression(
 1315            exp.BackupProperty(this=self._parse_var(any_token=True))
 1316        ),
 1317        "BLOCKCOMPRESSION": lambda self: self._parse_blockcompression(),
 1318        "CALLED": lambda self: self._parse_called_on_null_input_property(),
 1319        "CHARSET": lambda self, **kwargs: self._parse_character_set(**kwargs),
 1320        "CHECKSUM": lambda self: self._parse_checksum(),
 1321        "CLUSTER BY": lambda self: self._parse_cluster_property(),
 1322        "CLUSTERED": lambda self: self._parse_clustered_by(),
 1323        "COLLATE": lambda self, **kwargs: self._parse_property_assignment(
 1324            exp.CollateProperty, **kwargs
 1325        ),
 1326        "COMMENT": lambda self: self._parse_property_assignment(exp.SchemaCommentProperty),
 1327        "CONTAINS": lambda self: self._parse_contains_property(),
 1328        "COPY": lambda self: self._parse_copy_property(),
 1329        "DATABLOCKSIZE": lambda self, **kwargs: self._parse_datablocksize(**kwargs),
 1330        "DATA_DELETION": lambda self: self._parse_data_deletion_property(),
 1331        "DEFINER": lambda self: self._parse_definer(),
 1332        "DETERMINISTIC": lambda self: self.expression(
 1333            exp.StabilityProperty(this=exp.Literal.string("IMMUTABLE"))
 1334        ),
 1335        "DISTRIBUTED": lambda self: self._parse_distributed_property(),
 1336        "DUPLICATE": lambda self: self._parse_composite_key_property(exp.DuplicateKeyProperty),
 1337        "DYNAMIC": lambda self: self.expression(exp.DynamicProperty()),
 1338        "DISTKEY": lambda self: self._parse_distkey(),
 1339        "DISTSTYLE": lambda self: self._parse_property_assignment(exp.DistStyleProperty),
 1340        "EMPTY": lambda self: self.expression(exp.EmptyProperty()),
 1341        "ENGINE": lambda self: self._parse_property_assignment(exp.EngineProperty),
 1342        "ENVIRONMENT": lambda self: self.expression(
 1343            exp.EnviromentProperty(expressions=self._parse_wrapped_csv(self._parse_assignment))
 1344        ),
 1345        "HANDLER": lambda self: self._parse_property_assignment(exp.HandlerProperty),
 1346        "EXECUTE": lambda self: self._parse_property_assignment(exp.ExecuteAsProperty),
 1347        "EXTERNAL": lambda self: self.expression(exp.ExternalProperty()),
 1348        "FALLBACK": lambda self, **kwargs: self._parse_fallback(**kwargs),
 1349        "FORMAT": lambda self: self._parse_property_assignment(exp.FileFormatProperty),
 1350        "FREESPACE": lambda self: self._parse_freespace(),
 1351        "GLOBAL": lambda self: self.expression(exp.GlobalProperty()),
 1352        "HEAP": lambda self: self.expression(exp.HeapProperty()),
 1353        "ICEBERG": lambda self: self.expression(exp.IcebergProperty()),
 1354        "IMMUTABLE": lambda self: self.expression(
 1355            exp.StabilityProperty(this=exp.Literal.string("IMMUTABLE"))
 1356        ),
 1357        "INHERITS": lambda self: self.expression(
 1358            exp.InheritsProperty(expressions=self._parse_wrapped_csv(self._parse_table))
 1359        ),
 1360        "INPUT": lambda self: self.expression(exp.InputModelProperty(this=self._parse_schema())),
 1361        "JOURNAL": lambda self, **kwargs: self._parse_journal(**kwargs),
 1362        "LANGUAGE": lambda self: self._parse_property_assignment(exp.LanguageProperty),
 1363        "LAYOUT": lambda self: self._parse_dict_property(this="LAYOUT"),
 1364        "LIFETIME": lambda self: self._parse_dict_range(this="LIFETIME"),
 1365        "LIKE": lambda self: self._parse_create_like(),
 1366        "LOCATION": lambda self: self._parse_property_assignment(exp.LocationProperty),
 1367        "LOCK": lambda self: self._parse_locking(),
 1368        "LOCKING": lambda self: self._parse_locking(),
 1369        "LOG": lambda self, **kwargs: self._parse_log(**kwargs),
 1370        "MATERIALIZED": lambda self: self.expression(exp.MaterializedProperty()),
 1371        "MERGEBLOCKRATIO": lambda self, **kwargs: self._parse_mergeblockratio(**kwargs),
 1372        "MODIFIES": lambda self: self._parse_modifies_property(),
 1373        "MULTISET": lambda self: self.expression(exp.SetProperty(multi=True)),
 1374        "NO": lambda self: self._parse_no_property(),
 1375        "ON": lambda self: self._parse_on_property(),
 1376        "ORDER BY": lambda self: self._parse_order(skip_order_token=True),
 1377        "OUTPUT": lambda self: self.expression(exp.OutputModelProperty(this=self._parse_schema())),
 1378        "PARTITION": lambda self: self._parse_partitioned_of(),
 1379        "PARTITION BY": lambda self: self._parse_partitioned_by(),
 1380        "PARTITIONED BY": lambda self: self._parse_partitioned_by(),
 1381        "PARTITIONED_BY": lambda self: self._parse_partitioned_by(),
 1382        "PRIMARY KEY": lambda self: self._parse_primary_key(in_props=True),
 1383        "RANGE": lambda self: self._parse_dict_range(this="RANGE"),
 1384        "READS": lambda self: self._parse_reads_property(),
 1385        "REMOTE": lambda self: self._parse_remote_with_connection(),
 1386        "RETURNS": lambda self: self._parse_returns(),
 1387        "STRICT": lambda self: self.expression(exp.StrictProperty()),
 1388        "STREAMING": lambda self: self.expression(exp.StreamingTableProperty()),
 1389        "ROW": lambda self: self._parse_row(),
 1390        "ROW_FORMAT": lambda self: self._parse_property_assignment(exp.RowFormatProperty),
 1391        "SAMPLE": lambda self: self.expression(
 1392            exp.SampleProperty(this=self._match_text_seq("BY") and self._parse_bitwise())
 1393        ),
 1394        "SECURE": lambda self: self.expression(exp.SecureProperty()),
 1395        "SECURITY": lambda self: self._parse_sql_security(),
 1396        "SQL SECURITY": lambda self: self._parse_sql_security(),
 1397        "SET": lambda self: self.expression(exp.SetProperty(multi=False)),
 1398        "SETTINGS": lambda self: self._parse_settings_property(),
 1399        "SHARING": lambda self: self._parse_property_assignment(exp.SharingProperty),
 1400        "SORTKEY": lambda self: self._parse_sortkey(),
 1401        "SOURCE": lambda self: self._parse_dict_property(this="SOURCE"),
 1402        "STABLE": lambda self: self.expression(
 1403            exp.StabilityProperty(this=exp.Literal.string("STABLE"))
 1404        ),
 1405        "STORED": lambda self: self._parse_stored(),
 1406        "SYSTEM_VERSIONING": lambda self: self._parse_system_versioning_property(),
 1407        "TBLPROPERTIES": lambda self: self._parse_wrapped_properties(),
 1408        "TEMP": lambda self: self.expression(exp.TemporaryProperty()),
 1409        "TEMPORARY": lambda self: self.expression(exp.TemporaryProperty()),
 1410        "TO": lambda self: self._parse_to_table(),
 1411        "TRANSIENT": lambda self: self.expression(exp.TransientProperty()),
 1412        "TRANSFORM": lambda self: self.expression(
 1413            exp.TransformModelProperty(expressions=self._parse_wrapped_csv(self._parse_expression))
 1414        ),
 1415        "TTL": lambda self: self._parse_ttl(),
 1416        "USING": lambda self: self._parse_property_assignment(exp.FileFormatProperty),
 1417        "UNLOGGED": lambda self: self.expression(exp.UnloggedProperty()),
 1418        "VOLATILE": lambda self: self._parse_volatile_property(),
 1419        "WITH": lambda self: self._parse_with_property(),
 1420    }
 1421
 1422    CONSTRAINT_PARSERS: t.ClassVar = {
 1423        "AUTOINCREMENT": lambda self: self._parse_auto_increment(),
 1424        "AUTO_INCREMENT": lambda self: self._parse_auto_increment(),
 1425        "CASESPECIFIC": lambda self: self.expression(exp.CaseSpecificColumnConstraint(not_=False)),
 1426        "CHECK": lambda self: self._parse_check_constraint(),
 1427        "COLLATE": lambda self: self.expression(
 1428            exp.CollateColumnConstraint(this=self._parse_identifier() or self._parse_column())
 1429        ),
 1430        "COMMENT": lambda self: self.expression(
 1431            exp.CommentColumnConstraint(this=self._parse_string())
 1432        ),
 1433        "COMPRESS": lambda self: self._parse_compress(),
 1434        "CLUSTERED": lambda self: self.expression(
 1435            exp.ClusteredColumnConstraint(this=self._parse_wrapped_csv(self._parse_ordered))
 1436        ),
 1437        "NONCLUSTERED": lambda self: self.expression(
 1438            exp.NonClusteredColumnConstraint(this=self._parse_wrapped_csv(self._parse_ordered))
 1439        ),
 1440        "DEFAULT": lambda self: self.expression(
 1441            exp.DefaultColumnConstraint(this=self._parse_bitwise())
 1442        ),
 1443        "ENCODE": lambda self: self.expression(exp.EncodeColumnConstraint(this=self._parse_var())),
 1444        "EPHEMERAL": lambda self: self.expression(
 1445            exp.EphemeralColumnConstraint(this=self._parse_bitwise())
 1446        ),
 1447        "EXCLUDE": lambda self: self.expression(
 1448            exp.ExcludeColumnConstraint(this=self._parse_index_params())
 1449        ),
 1450        "FOREIGN KEY": lambda self: self._parse_foreign_key(),
 1451        "FORMAT": lambda self: self.expression(
 1452            exp.DateFormatColumnConstraint(this=self._parse_var_or_string())
 1453        ),
 1454        "GENERATED": lambda self: self._parse_generated_as_identity(),
 1455        "IDENTITY": lambda self: self._parse_auto_increment(),
 1456        "INLINE": lambda self: self._parse_inline(),
 1457        "LIKE": lambda self: self._parse_create_like(),
 1458        "NOT": lambda self: self._parse_not_constraint(),
 1459        "NULL": lambda self: self.expression(exp.NotNullColumnConstraint(allow_null=True)),
 1460        "ON": lambda self: (
 1461            (
 1462                self._match(TokenType.UPDATE)
 1463                and self.expression(exp.OnUpdateColumnConstraint(this=self._parse_function()))
 1464            )
 1465            or self.expression(exp.OnProperty(this=self._parse_id_var()))
 1466        ),
 1467        "PATH": lambda self: self.expression(exp.PathColumnConstraint(this=self._parse_string())),
 1468        "PERIOD": lambda self: self._parse_period_for_system_time(),
 1469        "PRIMARY KEY": lambda self: self._parse_primary_key(),
 1470        "REFERENCES": lambda self: self._parse_references(match=False),
 1471        "TITLE": lambda self: self.expression(
 1472            exp.TitleColumnConstraint(this=self._parse_var_or_string())
 1473        ),
 1474        "TTL": lambda self: self.expression(exp.MergeTreeTTL(expressions=[self._parse_bitwise()])),
 1475        "UNIQUE": lambda self: self._parse_unique(),
 1476        "UPPERCASE": lambda self: self.expression(exp.UppercaseColumnConstraint()),
 1477        "WITH": lambda self: self.expression(
 1478            exp.Properties(expressions=self._parse_wrapped_properties())
 1479        ),
 1480        "BUCKET": lambda self: self._parse_partitioned_by_bucket_or_truncate(),
 1481        "TRUNCATE": lambda self: self._parse_partitioned_by_bucket_or_truncate(),
 1482    }
 1483
 1484    def _parse_partitioned_by_bucket_or_truncate(self) -> exp.Expr | None:
 1485        if not self._match(TokenType.L_PAREN, advance=False):
 1486            # Partitioning by bucket or truncate follows the syntax:
 1487            # PARTITION BY (BUCKET(..) | TRUNCATE(..))
 1488            # If we don't have parenthesis after each keyword, we should instead parse this as an identifier
 1489            self._retreat(self._index - 1)
 1490            return None
 1491
 1492        klass = (
 1493            exp.PartitionedByBucket
 1494            if self._prev.text.upper() == "BUCKET"
 1495            else exp.PartitionByTruncate
 1496        )
 1497
 1498        args = self._parse_wrapped_csv(lambda: self._parse_primary() or self._parse_column())
 1499        this, expression = seq_get(args, 0), seq_get(args, 1)
 1500
 1501        if isinstance(this, exp.Literal):
 1502            # Check for Iceberg partition transforms (bucket / truncate) and ensure their arguments are in the right order
 1503            #  - For Hive, it's `bucket(<num buckets>, <col name>)` or `truncate(<num_chars>, <col_name>)`
 1504            #  - For Trino, it's reversed - `bucket(<col name>, <num buckets>)` or `truncate(<col_name>, <num_chars>)`
 1505            # Both variants are canonicalized in the latter i.e `bucket(<col name>, <num buckets>)`
 1506            #
 1507            # Hive ref: https://docs.aws.amazon.com/athena/latest/ug/querying-iceberg-creating-tables.html#querying-iceberg-partitioning
 1508            # Trino ref: https://docs.aws.amazon.com/athena/latest/ug/create-table-as.html#ctas-table-properties
 1509            this, expression = expression, this
 1510
 1511        return self.expression(klass(this=this, expression=expression))
 1512
 1513    ALTER_PARSERS: t.ClassVar = {
 1514        "ADD": lambda self: self._parse_alter_table_add(),
 1515        "AS": lambda self: self._parse_select(),
 1516        "ALTER": lambda self: self._parse_alter_table_alter(),
 1517        "CLUSTER BY": lambda self: self._parse_cluster_property(),
 1518        "DELETE": lambda self: self.expression(exp.Delete(where=self._parse_where())),
 1519        "DROP": lambda self: self._parse_alter_table_drop(),
 1520        "RENAME": lambda self: self._parse_alter_table_rename(),
 1521        "SET": lambda self: self._parse_alter_table_set(),
 1522        "SWAP": lambda self: self.expression(
 1523            exp.SwapTable(this=self._match(TokenType.WITH) and self._parse_table(schema=True))
 1524        ),
 1525    }
 1526
 1527    ALTER_ALTER_PARSERS: t.ClassVar = {
 1528        "DISTKEY": lambda self: self._parse_alter_diststyle(),
 1529        "DISTSTYLE": lambda self: self._parse_alter_diststyle(),
 1530        "SORTKEY": lambda self: self._parse_alter_sortkey(),
 1531        "COMPOUND": lambda self: self._parse_alter_sortkey(compound=True),
 1532    }
 1533
 1534    SCHEMA_UNNAMED_CONSTRAINTS: t.ClassVar = {
 1535        "CHECK",
 1536        "EXCLUDE",
 1537        "FOREIGN KEY",
 1538        "LIKE",
 1539        "PERIOD",
 1540        "PRIMARY KEY",
 1541        "UNIQUE",
 1542        "BUCKET",
 1543        "TRUNCATE",
 1544    }
 1545
 1546    NO_PAREN_FUNCTION_PARSERS: t.ClassVar = {
 1547        "ANY": lambda self: self.expression(exp.Any(this=self._parse_bitwise())),
 1548        "CASE": lambda self: self._parse_case(),
 1549        "CONNECT_BY_ROOT": lambda self: self.expression(
 1550            exp.ConnectByRoot(this=self._parse_column())
 1551        ),
 1552        "IF": lambda self: self._parse_if(),
 1553    }
 1554
 1555    INVALID_FUNC_NAME_TOKENS: t.ClassVar = {
 1556        TokenType.IDENTIFIER,
 1557        TokenType.STRING,
 1558    }
 1559
 1560    FUNCTIONS_WITH_ALIASED_ARGS: t.ClassVar = {"STRUCT"}
 1561
 1562    KEY_VALUE_DEFINITIONS: t.ClassVar = (exp.Alias, exp.EQ, exp.PropertyEQ, exp.Slice)
 1563
 1564    FUNCTION_PARSERS: t.ClassVar[dict[str, t.Callable]] = {
 1565        **{
 1566            name: lambda self: self._parse_distinct_arg_function(exp.ArgMax)
 1567            for name in exp.ArgMax.sql_names()
 1568        },
 1569        **{
 1570            name: lambda self: self._parse_distinct_arg_function(exp.ArgMin)
 1571            for name in exp.ArgMin.sql_names()
 1572        },
 1573        "CAST": lambda self: self._parse_cast(self.STRICT_CAST),
 1574        "CEIL": lambda self: self._parse_ceil_floor(exp.Ceil),
 1575        "CONVERT": lambda self: self._parse_convert(self.STRICT_CAST),
 1576        "CHAR": lambda self: self._parse_char(),
 1577        "CHR": lambda self: self._parse_char(),
 1578        "DECODE": lambda self: self._parse_decode(),
 1579        "EXTRACT": lambda self: self._parse_extract(),
 1580        "FLOOR": lambda self: self._parse_ceil_floor(exp.Floor),
 1581        "GAP_FILL": lambda self: self._parse_gap_fill(),
 1582        "INITCAP": lambda self: self._parse_initcap(),
 1583        "JSON_OBJECT": lambda self: self._parse_json_object(),
 1584        "JSON_OBJECTAGG": lambda self: self._parse_json_object(agg=True),
 1585        "JSON_TABLE": lambda self: self._parse_json_table(),
 1586        "MATCH": lambda self: self._parse_match_against(),
 1587        "NORMALIZE": lambda self: self._parse_normalize(),
 1588        "OPENJSON": lambda self: self._parse_open_json(),
 1589        "OVERLAY": lambda self: self._parse_overlay(),
 1590        "POSITION": lambda self: self._parse_position(),
 1591        "SAFE_CAST": lambda self: self._parse_cast(False, safe=True),
 1592        "STRING_AGG": lambda self: self._parse_string_agg(),
 1593        "SUBSTRING": lambda self: self._parse_substring(),
 1594        "TRIM": lambda self: self._parse_trim(),
 1595        "TRY_CAST": lambda self: self._parse_cast(False, safe=True),
 1596        "TRY_CONVERT": lambda self: self._parse_convert(False, safe=True),
 1597        "WEIGHT_STRING": lambda self: self._parse_weight_string(),
 1598        "XMLELEMENT": lambda self: self._parse_xml_element(),
 1599        "XMLTABLE": lambda self: self._parse_xml_table(),
 1600    }
 1601
 1602    QUERY_MODIFIER_PARSERS: t.ClassVar = {
 1603        TokenType.MATCH_RECOGNIZE: lambda self: ("match", self._parse_match_recognize()),
 1604        TokenType.PREWHERE: lambda self: ("prewhere", self._parse_prewhere()),
 1605        TokenType.WHERE: lambda self: ("where", self._parse_where()),
 1606        TokenType.GROUP_BY: lambda self: ("group", self._parse_group()),
 1607        TokenType.HAVING: lambda self: ("having", self._parse_having()),
 1608        TokenType.QUALIFY: lambda self: ("qualify", self._parse_qualify()),
 1609        TokenType.WINDOW: lambda self: ("windows", self._parse_window_clause()),
 1610        TokenType.ORDER_BY: lambda self: ("order", self._parse_order()),
 1611        TokenType.LIMIT: lambda self: ("limit", self._parse_limit()),
 1612        TokenType.FETCH: lambda self: ("limit", self._parse_limit()),
 1613        TokenType.OFFSET: lambda self: ("offset", self._parse_offset()),
 1614        TokenType.FOR: lambda self: ("locks", self._parse_locks()),
 1615        TokenType.LOCK: lambda self: ("locks", self._parse_locks()),
 1616        TokenType.TABLE_SAMPLE: lambda self: ("sample", self._parse_table_sample(as_modifier=True)),
 1617        TokenType.USING: lambda self: ("sample", self._parse_table_sample(as_modifier=True)),
 1618        TokenType.CLUSTER_BY: lambda self: (
 1619            "cluster",
 1620            self._parse_cluster(),
 1621        ),
 1622        TokenType.DISTRIBUTE_BY: lambda self: (
 1623            "distribute",
 1624            self._parse_sort(exp.Distribute, TokenType.DISTRIBUTE_BY),
 1625        ),
 1626        TokenType.SORT_BY: lambda self: ("sort", self._parse_sort(exp.Sort, TokenType.SORT_BY)),
 1627        TokenType.CONNECT_BY: lambda self: ("connect", self._parse_connect(skip_start_token=True)),
 1628    }
 1629    QUERY_MODIFIER_TOKENS: t.ClassVar = set(QUERY_MODIFIER_PARSERS)
 1630
 1631    SET_PARSERS: t.ClassVar = {
 1632        "GLOBAL": lambda self: self._parse_set_item_assignment("GLOBAL"),
 1633        "LOCAL": lambda self: self._parse_set_item_assignment("LOCAL"),
 1634        "SESSION": lambda self: self._parse_set_item_assignment("SESSION"),
 1635        "TRANSACTION": lambda self: self._parse_set_transaction(),
 1636    }
 1637
 1638    SHOW_PARSERS: t.ClassVar[dict[str, t.Callable]] = {}
 1639
 1640    TYPE_LITERAL_PARSERS: t.ClassVar = {
 1641        exp.DType.JSON: lambda self, this, _: self.expression(
 1642            exp.ParseJSON(this=this, is_literal=True)
 1643        ),
 1644    }
 1645
 1646    TYPE_CONVERTERS: t.ClassVar[dict[exp.DType, t.Callable[[exp.DataType], exp.DataType]]] = {}
 1647
 1648    DDL_SELECT_TOKENS: t.ClassVar = {TokenType.SELECT, TokenType.WITH, TokenType.L_PAREN}
 1649
 1650    PRE_VOLATILE_TOKENS: t.ClassVar = {TokenType.CREATE, TokenType.REPLACE, TokenType.UNIQUE}
 1651
 1652    TRANSACTION_KIND: t.ClassVar = {"DEFERRED", "IMMEDIATE", "EXCLUSIVE"}
 1653    TRANSACTION_CHARACTERISTICS: t.ClassVar[OPTIONS_TYPE] = {
 1654        "ISOLATION": (
 1655            ("LEVEL", "REPEATABLE", "READ"),
 1656            ("LEVEL", "READ", "COMMITTED"),
 1657            ("LEVEL", "READ", "UNCOMITTED"),
 1658            ("LEVEL", "SERIALIZABLE"),
 1659        ),
 1660        "READ": ("WRITE", "ONLY"),
 1661    }
 1662
 1663    CONFLICT_ACTIONS: t.ClassVar[OPTIONS_TYPE] = {
 1664        **dict.fromkeys(("ABORT", "FAIL", "IGNORE", "REPLACE", "ROLLBACK", "UPDATE"), tuple()),
 1665        "DO": ("NOTHING", "UPDATE"),
 1666    }
 1667
 1668    TRIGGER_TIMING: t.ClassVar[OPTIONS_TYPE] = {
 1669        "INSTEAD": (("OF",),),
 1670        "BEFORE": tuple(),
 1671        "AFTER": tuple(),
 1672    }
 1673
 1674    TRIGGER_DEFERRABLE: t.ClassVar[OPTIONS_TYPE] = {
 1675        "NOT": (("DEFERRABLE",),),
 1676        "DEFERRABLE": tuple(),
 1677    }
 1678
 1679    CREATE_SEQUENCE: t.ClassVar[OPTIONS_TYPE] = {
 1680        "SCALE": ("EXTEND", "NOEXTEND"),
 1681        "SHARD": ("EXTEND", "NOEXTEND"),
 1682        "NO": ("CYCLE", "CACHE", "MAXVALUE", "MINVALUE"),
 1683        **dict.fromkeys(
 1684            (
 1685                "SESSION",
 1686                "GLOBAL",
 1687                "KEEP",
 1688                "NOKEEP",
 1689                "ORDER",
 1690                "NOORDER",
 1691                "NOCACHE",
 1692                "CYCLE",
 1693                "NOCYCLE",
 1694                "NOMINVALUE",
 1695                "NOMAXVALUE",
 1696                "NOSCALE",
 1697                "NOSHARD",
 1698            ),
 1699            tuple(),
 1700        ),
 1701    }
 1702
 1703    ISOLATED_LOADING_OPTIONS: t.ClassVar[OPTIONS_TYPE] = {"FOR": ("ALL", "INSERT", "NONE")}
 1704
 1705    USABLES: t.ClassVar[OPTIONS_TYPE] = dict.fromkeys(
 1706        ("ROLE", "WAREHOUSE", "DATABASE", "SCHEMA", "CATALOG"), tuple()
 1707    )
 1708
 1709    CAST_ACTIONS: t.ClassVar[OPTIONS_TYPE] = dict.fromkeys(("RENAME", "ADD"), ("FIELDS",))
 1710
 1711    SCHEMA_BINDING_OPTIONS: t.ClassVar[OPTIONS_TYPE] = {
 1712        "TYPE": ("EVOLUTION",),
 1713        **dict.fromkeys(("BINDING", "COMPENSATION", "EVOLUTION"), tuple()),
 1714    }
 1715
 1716    PROCEDURE_OPTIONS: t.ClassVar[OPTIONS_TYPE] = {}
 1717
 1718    EXECUTE_AS_OPTIONS: t.ClassVar[OPTIONS_TYPE] = dict.fromkeys(
 1719        ("CALLER", "SELF", "OWNER"), tuple()
 1720    )
 1721
 1722    KEY_CONSTRAINT_OPTIONS: t.ClassVar[OPTIONS_TYPE] = {
 1723        "NOT": ("ENFORCED",),
 1724        "MATCH": (
 1725            "FULL",
 1726            "PARTIAL",
 1727            "SIMPLE",
 1728        ),
 1729        "INITIALLY": ("DEFERRED", "IMMEDIATE"),
 1730        "USING": (
 1731            "BTREE",
 1732            "HASH",
 1733        ),
 1734        **dict.fromkeys(("DEFERRABLE", "NORELY", "RELY"), tuple()),
 1735    }
 1736
 1737    WINDOW_EXCLUDE_OPTIONS: t.ClassVar[OPTIONS_TYPE] = {
 1738        "NO": ("OTHERS",),
 1739        "CURRENT": ("ROW",),
 1740        **dict.fromkeys(("GROUP", "TIES"), tuple()),
 1741    }
 1742
 1743    INSERT_ALTERNATIVES: t.ClassVar = {"ABORT", "FAIL", "IGNORE", "REPLACE", "ROLLBACK"}
 1744
 1745    CLONE_KEYWORDS: t.ClassVar = {"CLONE", "COPY"}
 1746    # Time travel clause prefixes, mapped to whether they pin a timestamp or a version
 1747    VERSION_PHRASES: t.ClassVar[dict[tuple[str, ...], str]] = {
 1748        ("FOR", "SYSTEM_TIME"): "TIMESTAMP",
 1749        ("FOR", "SYSTEM", "TIME"): "TIMESTAMP",
 1750        ("FOR", "TIMESTAMP"): "TIMESTAMP",
 1751        ("FOR", "VERSION"): "VERSION",
 1752        ("TIMESTAMP", "AS", "OF"): "TIMESTAMP",
 1753        ("VERSION", "AS", "OF"): "VERSION",
 1754    }
 1755
 1756    HISTORICAL_DATA_PREFIX: t.ClassVar = {"AT", "BEFORE", "END"}
 1757    HISTORICAL_DATA_KIND: t.ClassVar = {"OFFSET", "STATEMENT", "STREAM", "TIMESTAMP", "VERSION"}
 1758
 1759    OPCLASS_FOLLOW_KEYWORDS: t.ClassVar = {"ASC", "DESC", "NULLS", "WITH"}
 1760
 1761    OPTYPE_FOLLOW_TOKENS: t.ClassVar = {TokenType.COMMA, TokenType.R_PAREN}
 1762
 1763    TABLE_INDEX_HINT_TOKENS: t.ClassVar = {TokenType.FORCE, TokenType.IGNORE, TokenType.USE}
 1764
 1765    VIEW_ATTRIBUTES: t.ClassVar = {"ENCRYPTION", "SCHEMABINDING", "VIEW_METADATA"}
 1766
 1767    WINDOW_ALIAS_TOKENS: t.ClassVar = ID_VAR_TOKENS - {TokenType.RANGE, TokenType.ROWS}
 1768    WINDOW_BEFORE_PAREN_TOKENS: t.ClassVar = {TokenType.OVER}
 1769    WINDOW_SIDES: t.ClassVar = {"FOLLOWING", "PRECEDING"}
 1770
 1771    JSON_KEY_VALUE_SEPARATOR_TOKENS: t.ClassVar = {TokenType.COLON, TokenType.COMMA, TokenType.IS}
 1772
 1773    FETCH_TOKENS: t.ClassVar = ID_VAR_TOKENS - {TokenType.ROW, TokenType.ROWS, TokenType.PERCENT}
 1774
 1775    ADD_CONSTRAINT_TOKENS: t.ClassVar = {
 1776        TokenType.CONSTRAINT,
 1777        TokenType.FOREIGN_KEY,
 1778        TokenType.INDEX,
 1779        TokenType.KEY,
 1780        TokenType.PRIMARY_KEY,
 1781        TokenType.UNIQUE,
 1782    }
 1783
 1784    # Constraint kinds that start an ALTER ... ADD action but aren't tokens of their own
 1785    ADD_CONSTRAINT_KEYWORDS: t.ClassVar[set[str]] = set()
 1786
 1787    DISTINCT_TOKENS: t.ClassVar = {TokenType.DISTINCT}
 1788
 1789    UNNEST_OFFSET_ALIAS_TOKENS: t.ClassVar = TABLE_ALIAS_TOKENS - SET_OPERATIONS
 1790
 1791    SELECT_START_TOKENS: t.ClassVar = {TokenType.L_PAREN, TokenType.WITH, TokenType.SELECT}
 1792
 1793    COPY_INTO_VARLEN_OPTIONS: t.ClassVar = {
 1794        "FILE_FORMAT",
 1795        "COPY_OPTIONS",
 1796        "FORMAT_OPTIONS",
 1797        "CREDENTIAL",
 1798    }
 1799
 1800    IS_JSON_PREDICATE_KIND: t.ClassVar = {"VALUE", "SCALAR", "ARRAY", "OBJECT"}
 1801
 1802    ODBC_DATETIME_LITERALS: t.ClassVar[dict[str, type[exp.Expr]]] = {}
 1803
 1804    ON_CONDITION_TOKENS: t.ClassVar = {"ERROR", "NULL", "TRUE", "FALSE", "EMPTY"}
 1805
 1806    PRIVILEGE_FOLLOW_TOKENS: t.ClassVar = {TokenType.ON, TokenType.COMMA, TokenType.L_PAREN}
 1807
 1808    # The style options for the DESCRIBE statement
 1809    DESCRIBE_STYLES: t.ClassVar = {"ANALYZE", "EXTENDED", "FORMATTED", "HISTORY"}
 1810
 1811    SET_ASSIGNMENT_DELIMITERS: t.ClassVar = {"=", ":=", "TO"}
 1812
 1813    # The style options for the ANALYZE statement
 1814    ANALYZE_STYLES: t.ClassVar = {
 1815        "BUFFER_USAGE_LIMIT",
 1816        "FULL",
 1817        "LOCAL",
 1818        "NO_WRITE_TO_BINLOG",
 1819        "SAMPLE",
 1820        "SKIP_LOCKED",
 1821        "VERBOSE",
 1822    }
 1823
 1824    ANALYZE_EXPRESSION_PARSERS: t.ClassVar = {
 1825        "ALL": lambda self: self._parse_analyze_columns(),
 1826        "COMPUTE": lambda self: self._parse_analyze_statistics(),
 1827        "DELETE": lambda self: self._parse_analyze_delete(),
 1828        "DROP": lambda self: self._parse_analyze_histogram(),
 1829        "ESTIMATE": lambda self: self._parse_analyze_statistics(),
 1830        "LIST": lambda self: self._parse_analyze_list(),
 1831        "PREDICATE": lambda self: self._parse_analyze_columns(),
 1832        "UPDATE": lambda self: self._parse_analyze_histogram(),
 1833        "VALIDATE": lambda self: self._parse_analyze_validate(),
 1834    }
 1835
 1836    PARTITION_KEYWORDS: t.ClassVar = {"PARTITION", "SUBPARTITION"}
 1837
 1838    AMBIGUOUS_ALIAS_TOKENS: t.ClassVar = (TokenType.LIMIT, TokenType.OFFSET)
 1839
 1840    OPERATION_MODIFIERS: t.ClassVar[set[str]] = set()
 1841
 1842    RECURSIVE_CTE_SEARCH_KIND: t.ClassVar = {"BREADTH", "DEPTH", "CYCLE"}
 1843
 1844    SECURITY_PROPERTY_KEYWORDS: t.ClassVar = {"DEFINER", "INVOKER", "NONE"}
 1845
 1846    MODIFIABLES: t.ClassVar = (exp.Query, exp.Table, exp.TableFromRows, exp.Values)
 1847
 1848    STRICT_CAST: t.ClassVar = True
 1849
 1850    PREFIXED_PIVOT_COLUMNS: t.ClassVar = False
 1851    IDENTIFY_PIVOT_STRINGS: t.ClassVar = False
 1852    # Whether an UNPIVOT outputs its value column(s) before the name column
 1853    UNPIVOT_VALUE_COLUMNS_FIRST: t.ClassVar = False
 1854    # Controls when an aggregation's name is included in a pivoted column's name:
 1855    # "agg_name_if_aliased" - only for aggregations that carry an explicit alias
 1856    # "agg_name_if_aliased_or_multiple" - if aliased, or whenever there are multiple aggregations
 1857    # "agg_name_if_multiple" - only when there are multiple aggregations (a lone agg is value-only)
 1858    PIVOT_COLUMN_NAMING: t.ClassVar[str] = "agg_name_if_aliased"
 1859
 1860    LOG_DEFAULTS_TO_LN: t.ClassVar = False
 1861
 1862    # Whether the table sample clause expects CSV syntax
 1863    TABLESAMPLE_CSV: t.ClassVar = False
 1864
 1865    # The default method used for table sampling
 1866    DEFAULT_SAMPLING_METHOD: t.ClassVar[str | None] = None
 1867
 1868    # Whether the SET command needs a delimiter (e.g. "=") for assignments
 1869    SET_REQUIRES_ASSIGNMENT_DELIMITER: t.ClassVar = True
 1870
 1871    # Whether the TRIM function expects the characters to trim as its first argument
 1872    TRIM_PATTERN_FIRST: t.ClassVar = False
 1873
 1874    # Whether string aliases are supported `SELECT COUNT(*) 'count'`
 1875    STRING_ALIASES: t.ClassVar = False
 1876
 1877    # Whether query modifiers such as LIMIT are attached to the UNION node (vs its right operand)
 1878    MODIFIERS_ATTACHED_TO_SET_OP: t.ClassVar = True
 1879    SET_OP_MODIFIERS: t.ClassVar = {"order", "limit", "offset", "sort", "distribute", "cluster"}
 1880
 1881    # Whether to parse IF statements that aren't followed by a left parenthesis as commands
 1882    NO_PAREN_IF_COMMANDS: t.ClassVar = True
 1883
 1884    # Whether the -> and ->> operators expect documents of type JSON (e.g. Postgres)
 1885    JSON_ARROWS_REQUIRE_JSON_TYPE: t.ClassVar = False
 1886
 1887    # Whether the `:` operator is used to extract a value from a VARIANT column
 1888    COLON_IS_VARIANT_EXTRACT: t.ClassVar = False
 1889
 1890    # Whether a chain of colon extractions (x:y:z) is a single extraction with a merged
 1891    # path (x:y.z, e.g. Snowflake) or each colon extracts from the previous result (e.g. Databricks)
 1892    COLON_CHAIN_IS_SINGLE_EXTRACT: t.ClassVar = True
 1893
 1894    # Whether or not a VALUES keyword needs to be followed by '(' to form a VALUES clause.
 1895    # If this is True and '(' is not found, the keyword will be treated as an identifier
 1896    VALUES_FOLLOWED_BY_PAREN: t.ClassVar = True
 1897
 1898    # Whether implicit unnesting is supported, e.g. SELECT 1 FROM y.z AS z, z.a (Redshift)
 1899    SUPPORTS_IMPLICIT_UNNEST: t.ClassVar = False
 1900
 1901    # Whether field names can be digit-prefixed, e.g. data.144A_FLAG or data.144 (BigQuery)
 1902    SUPPORTS_DIGIT_PREFIXED_FIELD_NAMES: t.ClassVar = False
 1903
 1904    # Whether or not interval spans are supported, INTERVAL 1 YEAR TO MONTHS
 1905    INTERVAL_SPANS: t.ClassVar = True
 1906
 1907    # Whether a PARTITION clause can follow a table reference
 1908    SUPPORTS_PARTITION_SELECTION: t.ClassVar = False
 1909
 1910    # Whether the `name AS expr` schema/column constraint requires parentheses around `expr`
 1911    WRAPPED_TRANSFORM_COLUMN_CONSTRAINT: t.ClassVar = True
 1912
 1913    # Whether the 'AS' keyword is optional in the CTE definition syntax
 1914    OPTIONAL_ALIAS_TOKEN_CTE: t.ClassVar = True
 1915
 1916    # Whether renaming a column with an ALTER statement requires the presence of the COLUMN keyword
 1917    ALTER_RENAME_REQUIRES_COLUMN: t.ClassVar = True
 1918
 1919    # Whether dropping a column with an ALTER statement requires the presence of the COLUMN keyword
 1920    ALTER_DROP_REQUIRES_COLUMN: t.ClassVar = True
 1921
 1922    # Whether Alter statements are allowed to contain Partition specifications
 1923    ALTER_TABLE_PARTITIONS: t.ClassVar = False
 1924
 1925    # Whether an ALTER can combine different actions, e.g. ADD COLUMN a INT, DROP COLUMN b
 1926    ALTER_TABLE_MIXED_ACTIONS: t.ClassVar = False
 1927
 1928    # Whether an ALTER TABLE needs an action, e.g. MySQL's ALTER TABLE t ENGINE=InnoDB has none
 1929    ALTER_TABLE_REQUIRES_ACTION: t.ClassVar = True
 1930
 1931    # Whether changing a column's type with ALTER COLUMN requires the TYPE keyword
 1932    ALTER_COLUMN_TYPE_REQUIRES_KEYWORD: t.ClassVar = False
 1933
 1934    # Whether all join types have the same precedence, i.e., they "naturally" produce a left-deep tree.
 1935    # In standard SQL, joins that use the JOIN keyword take higher precedence than comma-joins. That is
 1936    # to say, JOIN operators happen before comma operators. This is not the case in some dialects, such
 1937    # as BigQuery, where all joins have the same precedence.
 1938    JOINS_HAVE_EQUAL_PRECEDENCE: t.ClassVar = False
 1939
 1940    # Whether TIMESTAMP <literal> can produce a zone-aware timestamp
 1941    ZONE_AWARE_TIMESTAMP_CONSTRUCTOR: t.ClassVar = False
 1942
 1943    # Whether map literals support arbitrary expressions as keys.
 1944    # When True, allows complex keys like arrays or literals: {[1, 2]: 3}, {1: 2} (e.g. DuckDB).
 1945    # When False, keys are typically restricted to identifiers.
 1946    MAP_KEYS_ARE_ARBITRARY_EXPRESSIONS: t.ClassVar = False
 1947
 1948    # Whether JSON_EXTRACT requires a JSON expression as the first argument, e.g this
 1949    # is true for Snowflake but not for BigQuery which can also process strings
 1950    JSON_EXTRACT_REQUIRES_JSON_EXPRESSION: t.ClassVar = False
 1951
 1952    # Dialects like Databricks support JOINS without join criteria
 1953    # Adding an ON TRUE, makes transpilation semantically correct for other dialects
 1954    ADD_JOIN_ON_TRUE: t.ClassVar = False
 1955
 1956    # Whether INTERVAL spans with literal format '\d+ hh:[mm:[ss[.ff]]]'
 1957    # can omit the span unit `DAY TO MINUTE` or `DAY TO SECOND`
 1958    SUPPORTS_OMITTED_INTERVAL_SPAN_UNIT: t.ClassVar = False
 1959
 1960    # Whether adjacent string literals like 'foo' 'bar' require a whitespace or comment between them
 1961    # to be considered valid syntactically. Such expressions evaluate to the strings' concatenation.
 1962    ADJACENT_STRINGS_CANNOT_BE_CONNECTED: t.ClassVar = False
 1963
 1964    # Whether NTH_VALUE accepts the FROM FIRST | LAST modifier before its OVER clause,
 1965    # e.g. NTH_VALUE(x, 2) FROM LAST IGNORE NULLS OVER (...) (Oracle, Snowflake)
 1966    SUPPORTS_NTH_VALUE_FROM_MODIFIER: t.ClassVar = False
 1967
 1968    # Type names that denote a different type when they're quoted, so quoting has to be
 1969    # preserved instead of resolving them into the built-in type of the same name. These
 1970    # are matched case sensitively, e.g. PostgreSQL's one-byte "char" is not CHAR
 1971    QUOTED_TYPES_TO_PRESERVE: t.ClassVar[set[str]] = set()
 1972
 1973    SHOW_TRIE: t.ClassVar[dict] = new_trie(key.split(" ") for key in SHOW_PARSERS)
 1974    SET_TRIE: t.ClassVar[dict] = new_trie(key.split(" ") for key in SET_PARSERS)
 1975
 1976    def __init__(
 1977        self,
 1978        error_level: ErrorLevel | None = None,
 1979        error_message_context: int = 100,
 1980        max_errors: int = 3,
 1981        max_nodes: int = -1,
 1982        dialect: DialectType = None,
 1983    ):
 1984        self.error_level: ErrorLevel = error_level or ErrorLevel.IMMEDIATE
 1985        self.error_message_context: int = error_message_context
 1986        self.max_errors: int = max_errors
 1987        self.max_nodes: int = max_nodes
 1988        self.dialect: t.Any = _resolve_dialect(dialect)
 1989        self.sql: str = ""
 1990        self.errors: list[ParseError] = []
 1991        self._tokens: list[Token] = []
 1992        self._tokens_size: i64 = 0
 1993        self._index: i64 = 0
 1994        self._curr: Token = SENTINEL_NONE
 1995        self._next: Token = SENTINEL_NONE
 1996        self._prev: Token = SENTINEL_NONE
 1997        self._prev_comments: list[str] = []
 1998        self._pipe_cte_counter: int = 0
 1999        self._chunks: list[list[Token]] = []
 2000        self._chunk_index: i64 = 0
 2001        self._node_count: int = 0
 2002
 2003    def reset(self) -> None:
 2004        self.sql = ""
 2005        self.errors = []
 2006        self._tokens = []
 2007        self._tokens_size = 0
 2008        self._index = 0
 2009        self._curr = SENTINEL_NONE
 2010        self._next = SENTINEL_NONE
 2011        self._prev = SENTINEL_NONE
 2012        self._prev_comments = []
 2013        self._pipe_cte_counter = 0
 2014        self._chunks = []
 2015        self._chunk_index = 0
 2016        self._node_count = 0
 2017
 2018    def _advance(self, times: i64 = 1) -> None:
 2019        index = self._index + times
 2020        self._index = index
 2021        tokens = self._tokens
 2022        size = self._tokens_size
 2023        self._curr = tokens[index] if index < size else SENTINEL_NONE
 2024        self._next = tokens[index + 1] if index + 1 < size else SENTINEL_NONE
 2025
 2026        if index > 0:
 2027            prev = tokens[index - 1]
 2028            self._prev = prev
 2029            self._prev_comments = prev.comments
 2030        else:
 2031            self._prev = SENTINEL_NONE
 2032            self._prev_comments = []
 2033
 2034    def _advance_chunk(self) -> None:
 2035        self._index = -1
 2036        self._tokens = self._chunks[self._chunk_index]
 2037        self._tokens_size = i64(len(self._tokens))
 2038        self._chunk_index += 1
 2039        self._advance()
 2040
 2041    def _retreat(self, index: i64) -> None:
 2042        if index != self._index:
 2043            self._advance(index - self._index)
 2044
 2045    def _add_comments(self, expression: exp.Expr | None) -> None:
 2046        if expression and self._prev_comments:
 2047            expression.add_comments(self._prev_comments)
 2048            self._prev_comments = []
 2049
 2050    def _match(
 2051        self, token_type: TokenType, advance: bool = True, expression: exp.Expr | None = None
 2052    ) -> bool:
 2053        if self._curr.token_type == token_type:
 2054            if advance:
 2055                self._advance()
 2056            self._add_comments(expression)
 2057            return True
 2058        return False
 2059
 2060    def _match_set(self, types: t.Collection[TokenType], advance: bool = True) -> bool:
 2061        if self._curr.token_type in types:
 2062            if advance:
 2063                self._advance()
 2064            return True
 2065        return False
 2066
 2067    def _match_pair(
 2068        self, token_type_a: TokenType, token_type_b: TokenType, advance: bool = True
 2069    ) -> bool:
 2070        if self._curr.token_type == token_type_a and self._next.token_type == token_type_b:
 2071            if advance:
 2072                self._advance(2)
 2073            return True
 2074        return False
 2075
 2076    def _match_texts(self, texts: TEXTS_TYPE, advance: bool = True) -> bool:
 2077        if (
 2078            self._curr.token_type not in self.TEXT_MATCH_EXCLUDED_TOKENS
 2079            and self._curr.text.upper() in texts
 2080        ):
 2081            if advance:
 2082                self._advance()
 2083            return True
 2084        return False
 2085
 2086    def _match_text_seq(self, *texts: str, advance: bool = True) -> bool:
 2087        index = self._index
 2088        excluded_tokens = self.TEXT_MATCH_EXCLUDED_TOKENS
 2089        for text in texts:
 2090            if self._curr.token_type not in excluded_tokens and self._curr.text.upper() == text:
 2091                self._advance()
 2092            else:
 2093                self._retreat(index)
 2094                return False
 2095
 2096        if not advance:
 2097            self._retreat(index)
 2098
 2099        return True
 2100
 2101    def _is_connected(self) -> bool:
 2102        prev = self._prev
 2103        curr = self._curr
 2104        return bool(prev and curr and prev.end + 1 == curr.start)
 2105
 2106    def _find_sql(self, start: Token, end: Token) -> str:
 2107        return self.sql[start.start : end.end + 1]
 2108
 2109    def raise_error(self, message: str, token: Token = SENTINEL_NONE) -> None:
 2110        token = token or self._curr or self._prev or Token.string("")
 2111        formatted_sql, start_context, highlight, end_context = highlight_sql(
 2112            sql=self.sql,
 2113            positions=[(token.start, token.end)],
 2114            context_length=self.error_message_context,
 2115        )
 2116        formatted_message = f"{message}. Line {token.line}, Col: {token.col}.\n  {formatted_sql}"
 2117
 2118        error = ParseError.new(
 2119            formatted_message,
 2120            description=message,
 2121            line=token.line,
 2122            col=token.col,
 2123            start_context=start_context,
 2124            highlight=highlight,
 2125            end_context=end_context,
 2126        )
 2127
 2128        if self.error_level == ErrorLevel.IMMEDIATE:
 2129            raise error
 2130
 2131        self.errors.append(error)
 2132
 2133    def validate_expression(self, expression: E, args: list | None = None) -> E:
 2134        if self.max_nodes > -1:
 2135            self._node_count += 1
 2136            if self._node_count > self.max_nodes:
 2137                self.raise_error(f"Maximum number of AST nodes ({self.max_nodes}) exceeded")
 2138        if self.error_level != ErrorLevel.IGNORE:
 2139            for error_message in expression.error_messages(args):
 2140                self.raise_error(error_message)
 2141        return expression
 2142
 2143    def _try_parse(self, parse_method: t.Callable[[], T], retreat: bool = False) -> T | None:
 2144        index = self._index
 2145        error_level = self.error_level
 2146        this: T | None = None
 2147
 2148        self.error_level = ErrorLevel.IMMEDIATE
 2149        try:
 2150            this = parse_method()
 2151        except ParseError:
 2152            this = None
 2153        finally:
 2154            if not this or retreat:
 2155                self._retreat(index)
 2156            self.error_level = error_level
 2157
 2158        return this
 2159
 2160    def parse(self, raw_tokens: list[Token], sql: str) -> list[exp.Expr | None]:
 2161        """
 2162        Parses a list of tokens and returns a list of syntax trees, one tree
 2163        per parsed SQL statement.
 2164
 2165        Args:
 2166            raw_tokens: The list of tokens.
 2167            sql: The original SQL string.
 2168
 2169        Returns:
 2170            The list of the produced syntax trees.
 2171        """
 2172        return self._parse(
 2173            parse_method=self.__class__._parse_statement, raw_tokens=raw_tokens, sql=sql
 2174        )
 2175
 2176    def parse_into(
 2177        self,
 2178        expression_types: exp.IntoType,
 2179        raw_tokens: list[Token],
 2180        sql: str | None = None,
 2181    ) -> list[exp.Expr | None]:
 2182        """
 2183        Parses a list of tokens into a given Expr type. If a collection of Expr
 2184        types is given instead, this method will try to parse the token list into each one
 2185        of them, stopping at the first for which the parsing succeeds.
 2186
 2187        Args:
 2188            expression_types: The expression type(s) to try and parse the token list into.
 2189            raw_tokens: The list of tokens.
 2190            sql: The original SQL string, used to produce helpful debug messages.
 2191
 2192        Returns:
 2193            The target Expr.
 2194        """
 2195        errors = []
 2196        for expression_type in ensure_list(expression_types):
 2197            parser = self.EXPRESSION_PARSERS.get(t.cast(type[exp.Expr], expression_type))
 2198            if not parser:
 2199                raise TypeError(f"No parser registered for {expression_type}")
 2200
 2201            try:
 2202                return self._parse(parser, raw_tokens, sql)
 2203            except ParseError as e:
 2204                e.errors[0]["into_expression"] = expression_type
 2205                errors.append(e)
 2206
 2207        raise ParseError(
 2208            f"Failed to parse '{sql or raw_tokens}' into {expression_types}",
 2209            errors=merge_errors(errors),
 2210        ) from errors[-1]
 2211
 2212    def check_errors(self) -> None:
 2213        """Logs or raises any found errors, depending on the chosen error level setting."""
 2214        if self.error_level == ErrorLevel.WARN:
 2215            for error in self.errors:
 2216                logger.error(str(error))
 2217        elif self.error_level == ErrorLevel.RAISE and self.errors:
 2218            raise ParseError(
 2219                concat_messages(self.errors, self.max_errors),
 2220                errors=merge_errors(self.errors),
 2221            )
 2222
 2223    def expression(
 2224        self,
 2225        instance: E,
 2226        token: Token | None = None,
 2227        comments: list[str] | None = None,
 2228    ) -> E:
 2229        if token:
 2230            instance.update_positions(token)
 2231        instance.add_comments(comments) if comments else self._add_comments(instance)
 2232        if not instance.is_primitive:
 2233            instance = self.validate_expression(instance)
 2234        return instance
 2235
 2236    def _parse_batch_statements(
 2237        self,
 2238        parse_method: t.Callable[[Parser], exp.Expr | None],
 2239        sep_first_statement: bool = True,
 2240    ) -> list[exp.Expr | None]:
 2241        expressions = []
 2242
 2243        # Chunkification binds if/while statements with the first statement of the body
 2244        if sep_first_statement:
 2245            self._match(TokenType.BEGIN)
 2246            expressions.append(parse_method(self))
 2247
 2248        chunks_length = len(self._chunks)
 2249        while self._chunk_index < chunks_length:
 2250            self._advance_chunk()
 2251
 2252            if self._match(TokenType.ELSE, advance=False):
 2253                return expressions
 2254
 2255            if expressions and not self._next and self._match(TokenType.END):
 2256                expressions.append(exp.EndStatement())
 2257                continue
 2258
 2259            expressions.append(parse_method(self))
 2260
 2261            if self._index < self._tokens_size:
 2262                self.raise_error("Invalid expression / Unexpected token")
 2263
 2264            self.check_errors()
 2265
 2266        return expressions
 2267
 2268    def _parse(
 2269        self,
 2270        parse_method: t.Callable[[Parser], exp.Expr | None],
 2271        raw_tokens: list[Token],
 2272        sql: str | None = None,
 2273    ) -> list[exp.Expr | None]:
 2274        self.reset()
 2275        self.sql = sql or ""
 2276
 2277        total = len(raw_tokens)
 2278        chunks: list[list[Token]] = [[]]
 2279
 2280        for i, token in enumerate(raw_tokens):
 2281            if token.token_type == TokenType.SEMICOLON:
 2282                if token.comments:
 2283                    chunks.append([token])
 2284
 2285                if i < total - 1:
 2286                    chunks.append([])
 2287            else:
 2288                chunks[-1].append(token)
 2289
 2290        self._chunks = chunks
 2291
 2292        return self._parse_batch_statements(parse_method=parse_method, sep_first_statement=False)
 2293
 2294    def _warn_unsupported(self) -> None:
 2295        if self._tokens_size <= 1:
 2296            return
 2297
 2298        # We use _find_sql because self.sql may comprise multiple chunks, and we're only
 2299        # interested in emitting a warning for the one being currently processed.
 2300        sql = self._find_sql(self._tokens[0], self._tokens[-1])[: self.error_message_context]
 2301
 2302        logger.warning(
 2303            f"'{sql}' contains unsupported syntax. Falling back to parsing as a 'Command'."
 2304        )
 2305
 2306    def _parse_command(self) -> exp.Command:
 2307        self._warn_unsupported()
 2308        comments = self._prev_comments
 2309        return self.expression(
 2310            exp.Command(this=self._prev.text.upper(), expression=self._parse_string()),
 2311            comments=comments,
 2312        )
 2313
 2314    def _parse_comment(self, allow_exists: bool = True) -> exp.Expr:
 2315        start = self._prev
 2316        exists = self._parse_exists() if allow_exists else None
 2317
 2318        self._match(TokenType.ON)
 2319
 2320        materialized = self._match_text_seq("MATERIALIZED")
 2321        kind = self._match_set(self.CREATABLES) and self._prev
 2322        if not kind:
 2323            return self._parse_as_command(start)
 2324
 2325        if kind.token_type in (TokenType.FUNCTION, TokenType.PROCEDURE):
 2326            this = self._parse_user_defined_function(kind=kind.token_type)
 2327        elif kind.token_type == TokenType.TABLE:
 2328            this = self._parse_table(alias_tokens=self.COMMENT_TABLE_ALIAS_TOKENS)
 2329        elif kind.token_type == TokenType.COLUMN:
 2330            this = self._parse_column()
 2331        else:
 2332            this = self._parse_table_parts(schema=True)
 2333
 2334        self._match(TokenType.IS)
 2335
 2336        return self.expression(
 2337            exp.Comment(
 2338                this=this,
 2339                kind=kind.text,
 2340                expression=self._parse_string(),
 2341                exists=exists,
 2342                materialized=materialized,
 2343            )
 2344        )
 2345
 2346    def _parse_to_table(
 2347        self,
 2348    ) -> exp.ToTableProperty:
 2349        table = self._parse_table_parts(schema=True)
 2350        return self.expression(exp.ToTableProperty(this=table))
 2351
 2352    # https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/mergetree#mergetree-table-ttl
 2353    def _parse_ttl(self) -> exp.Expr:
 2354        def _parse_ttl_action() -> exp.Expr | None:
 2355            this = self._parse_bitwise()
 2356
 2357            if self._match_text_seq("DELETE"):
 2358                return self.expression(exp.MergeTreeTTLAction(this=this, delete=True))
 2359            if self._match_text_seq("RECOMPRESS"):
 2360                return self.expression(
 2361                    exp.MergeTreeTTLAction(this=this, recompress=self._parse_bitwise())
 2362                )
 2363            if self._match_text_seq("TO", "DISK"):
 2364                return self.expression(
 2365                    exp.MergeTreeTTLAction(this=this, to_disk=self._parse_string())
 2366                )
 2367            if self._match_text_seq("TO", "VOLUME"):
 2368                return self.expression(
 2369                    exp.MergeTreeTTLAction(this=this, to_volume=self._parse_string())
 2370                )
 2371
 2372            return this
 2373
 2374        expressions = self._parse_csv(_parse_ttl_action)
 2375        where = self._parse_where()
 2376        group = self._parse_group()
 2377
 2378        aggregates = None
 2379        if group and self._match(TokenType.SET):
 2380            aggregates = self._parse_csv(self._parse_set_item)
 2381
 2382        return self.expression(
 2383            exp.MergeTreeTTL(
 2384                expressions=expressions, where=where, group=group, aggregates=aggregates
 2385            )
 2386        )
 2387
 2388    def _parse_condition(self) -> exp.Expr | None:
 2389        return self._parse_wrapped(parse_method=self._parse_expression, optional=True)
 2390
 2391    def _parse_block(self) -> exp.Block:
 2392        return self.expression(
 2393            exp.Block(
 2394                expressions=self._parse_batch_statements(
 2395                    parse_method=lambda self: self._parse_statement()
 2396                )
 2397            )
 2398        )
 2399
 2400    def _parse_whileblock(self) -> exp.WhileBlock:
 2401        return self.expression(
 2402            exp.WhileBlock(this=self._parse_condition(), body=self._parse_block())
 2403        )
 2404
 2405    def _parse_statement(self) -> exp.Expr | None:
 2406        if not self._curr:
 2407            return None
 2408
 2409        if self._match_set(self.STATEMENT_PARSERS):
 2410            comments = self._prev_comments
 2411            stmt = self.STATEMENT_PARSERS[self._prev.token_type](self)
 2412            stmt.add_comments(comments, prepend=True)
 2413            return stmt
 2414
 2415        if self._match_set(self.dialect.tokenizer_class.COMMANDS):
 2416            return self._parse_command()
 2417
 2418        if self._match_text_seq("WHILE"):
 2419            return self._parse_whileblock()
 2420
 2421        expression = self._parse_expression()
 2422        expression = self._parse_set_operations(expression) if expression else self._parse_select()
 2423
 2424        if isinstance(expression, exp.Subquery) and self._match(TokenType.PIPE_GT, advance=False):
 2425            expression = self._parse_pipe_syntax_query(expression)
 2426
 2427        return self._parse_query_modifiers(expression)
 2428
 2429    def _parse_drop(self, exists: bool = False, kind: str | None = None) -> exp.Drop | exp.Command:
 2430        start = self._prev
 2431
 2432        if not kind:
 2433            temporary = self._match(TokenType.TEMPORARY)
 2434            materialized = self._match_text_seq("MATERIALIZED")
 2435            iceberg = self._match_text_seq("ICEBERG")
 2436            kind = self._prev.text.upper() if self._match_set(self.CREATABLES) else None
 2437        else:
 2438            temporary = False
 2439            materialized = False
 2440            iceberg = False
 2441
 2442        if not kind or (iceberg and kind != "TABLE"):
 2443            return self._parse_as_command(start)
 2444
 2445        concurrently = (
 2446            kind == "INDEX"
 2447            and self._next.token_type in self.ID_VAR_TOKENS
 2448            and self._match_text_seq("CONCURRENTLY")
 2449        )
 2450
 2451        if_exists = exists or self._parse_exists()
 2452
 2453        tables: exp.Expr | list[exp.Expr] | None
 2454        if kind == "COLUMN":
 2455            # A dropped column named after a keyword, like `any` or `charset`, is only a name
 2456            name = (
 2457                None
 2458                if self._curr.text.upper() in self.NO_PAREN_FUNCTION_PARSERS
 2459                else self._parse_column()
 2460            ) or self._parse_id_var()
 2461            tables = (
 2462                self.expression(exp.Column(this=name)) if isinstance(name, exp.Identifier) else name
 2463            )
 2464        elif kind in ("TABLE", "VIEW"):
 2465            tables = self._parse_csv(lambda: self._parse_table_parts(schema=True))
 2466        else:
 2467            tables = self._parse_table_parts(schema=True, is_db_reference=kind == "SCHEMA")
 2468
 2469        cluster: exp.Expr | None = None
 2470        if self._match(TokenType.ON):
 2471            if kind == "INDEX":
 2472                cluster = self.expression(exp.OnProperty(this=self._parse_table_parts()))
 2473            else:
 2474                cluster = self._parse_on_property()
 2475
 2476        if self._match(TokenType.L_PAREN, advance=False):
 2477            expressions = self._parse_wrapped_csv(self._parse_types)
 2478        else:
 2479            expressions = None
 2480
 2481        cascade_or_restrict = self._match_texts(("CASCADE", "RESTRICT")) and self._prev.text.upper()
 2482
 2483        return self.expression(
 2484            exp.Drop(
 2485                exists=if_exists,
 2486                tables=ensure_list(tables),
 2487                expressions=expressions,
 2488                kind=self.dialect.CREATABLE_KIND_MAPPING.get(kind) or kind,
 2489                temporary=temporary,
 2490                materialized=materialized,
 2491                cascade=cascade_or_restrict == "CASCADE",
 2492                restrict=cascade_or_restrict == "RESTRICT",
 2493                constraints=self._match_text_seq("CONSTRAINTS"),
 2494                purge=self._match_text_seq("PURGE"),
 2495                cluster=cluster,
 2496                concurrently=concurrently,
 2497                sync=self._match_text_seq("SYNC"),
 2498                iceberg=iceberg,
 2499                force=self._match_text_seq("FORCE"),
 2500            )
 2501        )
 2502
 2503    def _parse_exists(self, not_: bool = False) -> bool | None:
 2504        if not_:
 2505            return self._match_text_seq("IF", "NOT", "EXISTS")
 2506        return self._match_text_seq("IF", "EXISTS")
 2507
 2508    def _parse_create(self) -> exp.Create | exp.Command:
 2509        # Note: this can't be None because we've matched a statement parser
 2510        start = self._prev
 2511
 2512        replace = (
 2513            start.token_type == TokenType.REPLACE
 2514            or self._match_pair(TokenType.OR, TokenType.REPLACE)
 2515            or self._match_pair(TokenType.OR, TokenType.ALTER)
 2516        )
 2517        refresh = self._match_pair(TokenType.OR, TokenType.REFRESH)
 2518
 2519        unique = self._match(TokenType.UNIQUE)
 2520
 2521        if self._match_text_seq("CLUSTERED", "COLUMNSTORE"):
 2522            clustered = True
 2523        elif self._match_text_seq("NONCLUSTERED", "COLUMNSTORE") or self._match_text_seq(
 2524            "COLUMNSTORE"
 2525        ):
 2526            clustered = False
 2527        else:
 2528            clustered = None
 2529
 2530        if self._match_pair(TokenType.TABLE, TokenType.FUNCTION, advance=False):
 2531            self._advance()
 2532
 2533        properties = None
 2534        create_token = self._match_set(self.CREATABLES) and self._prev
 2535
 2536        if not create_token:
 2537            # exp.Properties.Location.POST_CREATE
 2538            properties = self._parse_properties()
 2539            create_token = self._match_set(self.CREATABLES) and self._prev
 2540
 2541            if not properties or not create_token:
 2542                return self._parse_as_command(start)
 2543
 2544        create_token_type = t.cast(Token, create_token).token_type
 2545
 2546        concurrently = self._match_text_seq("CONCURRENTLY")
 2547        exists = self._parse_exists(not_=True)
 2548        this = None
 2549        expression: exp.Expr | None = None
 2550        indexes = None
 2551        no_schema_binding = None
 2552        begin = None
 2553        clone = None
 2554
 2555        def extend_props(temp_props: exp.Properties | None) -> None:
 2556            nonlocal properties
 2557            if properties and temp_props:
 2558                properties.expressions.extend(temp_props.expressions)
 2559            elif temp_props:
 2560                properties = temp_props
 2561
 2562        if create_token_type in (TokenType.FUNCTION, TokenType.PROCEDURE):
 2563            this = self._parse_user_defined_function(kind=create_token_type)
 2564
 2565            # exp.Properties.Location.POST_SCHEMA ("schema" here is the UDF's type signature)
 2566            extend_props(self._parse_properties())
 2567
 2568            expression = self._parse_heredoc() if self._match(TokenType.ALIAS) else None
 2569
 2570            if (
 2571                not expression
 2572                and create_token_type == TokenType.FUNCTION
 2573                and isinstance(this, exp.UserDefinedFunction)
 2574                and this.args.get("wrapped")
 2575            ):
 2576                pre_table_index = self._index
 2577                is_table = self._match(TokenType.TABLE)
 2578
 2579                expression = self._parse_expression()
 2580                overload_mode = bool(
 2581                    expression
 2582                    and self._curr.token_type == TokenType.COMMA
 2583                    and self._next.token_type == TokenType.L_PAREN
 2584                )
 2585                if not overload_mode:
 2586                    self._retreat(pre_table_index)
 2587                    is_table = False
 2588                    expression = None
 2589            else:
 2590                is_table = False
 2591                overload_mode = False
 2592
 2593            extend_props(self._parse_function_properties())
 2594
 2595            if not expression:
 2596                if self._match(TokenType.COMMAND):
 2597                    expression = self._parse_as_command(self._prev)
 2598                else:
 2599                    begin = self._match(TokenType.BEGIN)
 2600                    return_ = self._match_text_seq("RETURN")
 2601
 2602                    if self._match(TokenType.STRING, advance=False):
 2603                        # Takes care of BigQuery's JavaScript UDF definitions that end in an OPTIONS property
 2604                        # # https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_function_statement
 2605                        expression = self._parse_string()
 2606                        extend_props(self._parse_properties())
 2607                    else:
 2608                        expression = (
 2609                            self._parse_user_defined_function_expression()
 2610                            if create_token_type == TokenType.FUNCTION
 2611                            else self._parse_block()
 2612                        )
 2613
 2614                    if return_:
 2615                        expression = self.expression(exp.Return(this=expression))
 2616
 2617            if overload_mode and expression:
 2618                expression = self._parse_macro_overloads(
 2619                    t.cast(exp.UserDefinedFunction, this), expression, is_table
 2620                )
 2621        elif create_token_type == TokenType.INDEX:
 2622            # Postgres allows anonymous indexes, eg. CREATE INDEX IF NOT EXISTS ON t(c)
 2623            if not self._match(TokenType.ON):
 2624                index = self._parse_id_var()
 2625                anonymous = False
 2626            else:
 2627                index = None
 2628                anonymous = True
 2629
 2630            this = self._parse_index(index=index, anonymous=anonymous)
 2631        elif (
 2632            create_token_type == TokenType.CONSTRAINT and self._match(TokenType.TRIGGER)
 2633        ) or create_token_type == TokenType.TRIGGER:
 2634            if is_constraint := (create_token_type == TokenType.CONSTRAINT):
 2635                create_token = self._prev
 2636
 2637            trigger_name = self._parse_id_var()
 2638            if not trigger_name:
 2639                return self._parse_as_command(start)
 2640
 2641            timing_var = self._parse_var_from_options(self.TRIGGER_TIMING, raise_unmatched=False)
 2642            timing = timing_var.this if timing_var else None
 2643            if not timing:
 2644                return self._parse_as_command(start)
 2645
 2646            events = self._parse_trigger_events()
 2647            if not self._match(TokenType.ON):
 2648                self.raise_error("Expected ON in trigger definition")
 2649
 2650            table = self._parse_table_parts()
 2651            referenced_table = self._parse_table_parts() if self._match(TokenType.FROM) else None
 2652            deferrable, initially = self._parse_trigger_deferrable()
 2653            referencing = self._parse_trigger_referencing()
 2654            for_each = self._parse_trigger_for_each()
 2655            when = self._match_text_seq("WHEN") and self._parse_wrapped(
 2656                self._parse_disjunction, optional=True
 2657            )
 2658            execute = self._parse_trigger_execute()
 2659
 2660            if execute is None:
 2661                return self._parse_as_command(start)
 2662
 2663            trigger_props = self.expression(
 2664                exp.TriggerProperties(
 2665                    table=table,
 2666                    timing=timing,
 2667                    events=events,
 2668                    execute=execute,
 2669                    constraint=is_constraint,
 2670                    referenced_table=referenced_table,
 2671                    deferrable=deferrable,
 2672                    initially=initially,
 2673                    referencing=referencing,
 2674                    for_each=for_each,
 2675                    when=when,
 2676                )
 2677            )
 2678
 2679            this = trigger_name
 2680            extend_props(exp.Properties(expressions=[trigger_props] if trigger_props else []))
 2681        elif create_token_type == TokenType.TYPE:
 2682            this = self._parse_table_parts(schema=True)
 2683            if not this or not self._match(TokenType.ALIAS):
 2684                return self._parse_as_command(start)
 2685
 2686            if self._match(TokenType.ENUM):
 2687                expression = exp.DataType(
 2688                    this=exp.DType.ENUM,
 2689                    expressions=self._parse_wrapped_csv(self._parse_string),
 2690                )
 2691            elif self._match(TokenType.L_PAREN, advance=False):
 2692                expression = self._parse_schema()
 2693            else:
 2694                return self._parse_as_command(start)
 2695        elif create_token_type in self.DB_CREATABLES:
 2696            table_parts = self._parse_table_parts(
 2697                schema=True, is_db_reference=create_token_type == TokenType.SCHEMA
 2698            )
 2699
 2700            # exp.Properties.Location.POST_NAME
 2701            self._match(TokenType.COMMA)
 2702            extend_props(self._parse_properties(before=True))
 2703
 2704            this = self._parse_schema(this=table_parts)
 2705
 2706            # exp.Properties.Location.POST_SCHEMA and POST_WITH
 2707            extend_props(self._parse_properties())
 2708
 2709            has_alias = self._match(TokenType.ALIAS)
 2710            if not self._match_set(self.DDL_SELECT_TOKENS, advance=False):
 2711                # exp.Properties.Location.POST_ALIAS
 2712                extend_props(self._parse_properties())
 2713
 2714            if create_token_type == TokenType.SEQUENCE:
 2715                expression = self._parse_types()
 2716                props = self._parse_properties()
 2717                if props:
 2718                    sequence_props = exp.SequenceProperties()
 2719                    options = []
 2720                    for prop in props:
 2721                        if isinstance(prop, exp.SequenceProperties):
 2722                            for arg, value in prop.args.items():
 2723                                if arg == "options":
 2724                                    options.extend(value)
 2725                                else:
 2726                                    sequence_props.set(arg, value)
 2727                            prop.pop()
 2728
 2729                    if options:
 2730                        sequence_props.set("options", options)
 2731
 2732                    props.append("expressions", sequence_props)
 2733                    extend_props(props)
 2734            else:
 2735                expression = self._parse_ddl_select()
 2736
 2737                # Some dialects also support using a table as an alias instead of a SELECT.
 2738                # Here we fallback to this as an alternative.
 2739                if not expression and has_alias:
 2740                    expression = self._try_parse(self._parse_table_parts)
 2741
 2742            if create_token_type == TokenType.TABLE:
 2743                # exp.Properties.Location.POST_EXPRESSION
 2744                extend_props(self._parse_properties())
 2745
 2746                indexes = []
 2747                while True:
 2748                    index = self._parse_index()
 2749
 2750                    # exp.Properties.Location.POST_INDEX
 2751                    extend_props(self._parse_properties())
 2752                    if not index:
 2753                        break
 2754                    else:
 2755                        self._match(TokenType.COMMA)
 2756                        indexes.append(index)
 2757            elif create_token_type == TokenType.VIEW:
 2758                if self._match_text_seq("WITH", "NO", "SCHEMA", "BINDING"):
 2759                    no_schema_binding = True
 2760            elif create_token_type in (TokenType.SINK, TokenType.SOURCE):
 2761                extend_props(self._parse_properties())
 2762
 2763            shallow = self._match_text_seq("SHALLOW")
 2764
 2765            if self._match_texts(self.CLONE_KEYWORDS):
 2766                copy = self._prev.text.lower() == "copy"
 2767                clone = self.expression(
 2768                    exp.Clone(this=self._parse_table(schema=True), shallow=shallow, copy=copy)
 2769                )
 2770
 2771        if self._curr and not self._match_set((TokenType.R_PAREN, TokenType.COMMA), advance=False):
 2772            return self._parse_as_command(start)
 2773
 2774        create_kind_text = create_token.text.upper()
 2775        return self.expression(
 2776            exp.Create(
 2777                this=this,
 2778                kind=self.dialect.CREATABLE_KIND_MAPPING.get(create_kind_text) or create_kind_text,
 2779                replace=replace,
 2780                refresh=refresh,
 2781                unique=unique,
 2782                expression=expression,
 2783                exists=exists,
 2784                properties=properties,
 2785                indexes=indexes,
 2786                no_schema_binding=no_schema_binding,
 2787                begin=begin,
 2788                clone=clone,
 2789                concurrently=concurrently,
 2790                clustered=clustered,
 2791            )
 2792        )
 2793
 2794    def _parse_sequence_properties(self) -> exp.SequenceProperties | None:
 2795        seq = exp.SequenceProperties()
 2796
 2797        options = []
 2798        index = self._index
 2799
 2800        while self._curr:
 2801            self._match(TokenType.COMMA)
 2802            if self._match_text_seq("INCREMENT"):
 2803                self._match_text_seq("BY")
 2804                self._match_text_seq("=")
 2805                seq.set("increment", self._parse_term())
 2806            elif self._match_text_seq("MINVALUE"):
 2807                seq.set("minvalue", self._parse_term())
 2808            elif self._match_text_seq("MAXVALUE"):
 2809                seq.set("maxvalue", self._parse_term())
 2810            elif self._match_text_seq("START"):
 2811                self._match_text_seq("WITH")
 2812                self._match_text_seq("=")
 2813                seq.set("start", self._parse_term())
 2814            elif self._match_text_seq("CACHE"):
 2815                # T-SQL allows empty CACHE which is initialized dynamically
 2816                seq.set("cache", self._parse_number() or True)
 2817            elif self._match_text_seq("OWNED", "BY"):
 2818                # "OWNED BY NONE" is the default
 2819                seq.set("owned", None if self._match_text_seq("NONE") else self._parse_column())
 2820            else:
 2821                opt = self._parse_var_from_options(self.CREATE_SEQUENCE, raise_unmatched=False)
 2822                if opt:
 2823                    options.append(opt)
 2824                else:
 2825                    break
 2826
 2827        seq.set("options", options if options else None)
 2828        return None if self._index == index else seq
 2829
 2830    def _parse_trigger_events(self) -> list[exp.TriggerEvent]:
 2831        events = []
 2832
 2833        while True:
 2834            event_type = self._match_set(self.TRIGGER_EVENTS) and self._prev.text.upper()
 2835
 2836            if not event_type:
 2837                self.raise_error("Expected trigger event (INSERT, UPDATE, DELETE, TRUNCATE)")
 2838
 2839            columns = (
 2840                self._parse_csv(self._parse_column)
 2841                if event_type == "UPDATE" and self._match_text_seq("OF")
 2842                else None
 2843            )
 2844
 2845            events.append(self.expression(exp.TriggerEvent(this=event_type, columns=columns)))
 2846
 2847            if not self._match(TokenType.OR):
 2848                break
 2849
 2850        return events
 2851
 2852    def _parse_trigger_deferrable(
 2853        self,
 2854    ) -> tuple[str | None, str | None]:
 2855        deferrable_var = self._parse_var_from_options(
 2856            self.TRIGGER_DEFERRABLE, raise_unmatched=False
 2857        )
 2858        deferrable = deferrable_var.this if deferrable_var else None
 2859
 2860        initially = None
 2861        if deferrable and self._match_text_seq("INITIALLY"):
 2862            initially = (
 2863                self._prev.text.upper() if self._match_texts(("IMMEDIATE", "DEFERRED")) else None
 2864            )
 2865
 2866        return deferrable, initially
 2867
 2868    def _parse_trigger_referencing_clause(self, keyword: str) -> exp.Expr | None:
 2869        if not self._match_text_seq(keyword):
 2870            return None
 2871        if not self._match_text_seq("TABLE"):
 2872            self.raise_error(f"Expected TABLE after {keyword} in REFERENCING clause")
 2873        self._match_text_seq("AS")
 2874        return self._parse_id_var()
 2875
 2876    def _parse_trigger_referencing(self) -> exp.TriggerReferencing | None:
 2877        if not self._match_text_seq("REFERENCING"):
 2878            return None
 2879
 2880        old_alias = None
 2881        new_alias = None
 2882
 2883        while True:
 2884            if alias := self._parse_trigger_referencing_clause("OLD"):
 2885                if old_alias is not None:
 2886                    self.raise_error("Duplicate OLD clause in REFERENCING")
 2887                old_alias = alias
 2888            elif alias := self._parse_trigger_referencing_clause("NEW"):
 2889                if new_alias is not None:
 2890                    self.raise_error("Duplicate NEW clause in REFERENCING")
 2891                new_alias = alias
 2892            else:
 2893                break
 2894
 2895        if old_alias is None and new_alias is None:
 2896            self.raise_error("REFERENCING clause requires at least OLD TABLE or NEW TABLE")
 2897
 2898        return self.expression(exp.TriggerReferencing(old=old_alias, new=new_alias))
 2899
 2900    def _parse_trigger_for_each(self) -> str | None:
 2901        if not self._match_text_seq("FOR", "EACH"):
 2902            return None
 2903
 2904        return self._prev.text.upper() if self._match_texts(("ROW", "STATEMENT")) else None
 2905
 2906    def _parse_trigger_execute(self) -> exp.TriggerExecute | None:
 2907        if not self._match(TokenType.EXECUTE):
 2908            return None
 2909
 2910        if not self._match_set((TokenType.FUNCTION, TokenType.PROCEDURE)):
 2911            self.raise_error("Expected FUNCTION or PROCEDURE after EXECUTE")
 2912
 2913        func_call = self._parse_column()
 2914        return self.expression(exp.TriggerExecute(this=func_call))
 2915
 2916    def _parse_property_before(self) -> exp.Expr | list[exp.Expr] | None:
 2917        # only used for teradata currently
 2918        self._match(TokenType.COMMA)
 2919
 2920        kwargs = {
 2921            "no": self._match_text_seq("NO"),
 2922            "dual": self._match_text_seq("DUAL"),
 2923            "before": self._match_text_seq("BEFORE"),
 2924            "default": self._match_text_seq("DEFAULT"),
 2925            "local": (self._match_text_seq("LOCAL") and "LOCAL")
 2926            or (self._match_text_seq("NOT", "LOCAL") and "NOT LOCAL"),
 2927            "after": self._match_text_seq("AFTER"),
 2928            "minimum": self._match_texts(("MIN", "MINIMUM")),
 2929            "maximum": self._match_texts(("MAX", "MAXIMUM")),
 2930        }
 2931
 2932        if self._match_texts(self.PROPERTY_PARSERS):
 2933            parser = self.PROPERTY_PARSERS[self._prev.text.upper()]
 2934            try:
 2935                return parser(self, **{k: v for k, v in kwargs.items() if v})
 2936            except TypeError:
 2937                self.raise_error(f"Cannot parse property '{self._prev.text}'")
 2938
 2939        if self._match_pair(TokenType.CHAR, TokenType.SET):
 2940            return self._parse_character_set(default=bool(kwargs["default"]))
 2941
 2942        return None
 2943
 2944    def _parse_wrapped_properties(self) -> list[exp.Expr | list[exp.Expr]]:
 2945        return self._parse_wrapped_csv(self._parse_property)
 2946
 2947    def _parse_property(self) -> exp.Expr | list[exp.Expr] | None:
 2948        if self._match_texts(self.PROPERTY_PARSERS):
 2949            return self.PROPERTY_PARSERS[self._prev.text.upper()](self)
 2950
 2951        if self._match_pair(TokenType.CHAR, TokenType.SET):
 2952            return self._parse_character_set()
 2953
 2954        if self._match(TokenType.DEFAULT):
 2955            if self._match_texts(self.PROPERTY_PARSERS):
 2956                return self.PROPERTY_PARSERS[self._prev.text.upper()](self, default=True)
 2957
 2958            if self._match_pair(TokenType.CHAR, TokenType.SET):
 2959                return self._parse_character_set(default=True)
 2960
 2961        if self._match_text_seq("COMPOUND", "SORTKEY"):
 2962            return self._parse_sortkey(compound=True)
 2963
 2964        if self._match_text_seq("PARAMETER", "STYLE", "PANDAS"):
 2965            return self.expression(exp.ParameterStyleProperty(this="PANDAS"))
 2966
 2967        if self._match_text_seq("NOT", "DETERMINISTIC"):
 2968            return self.expression(exp.StabilityProperty(this=exp.Literal.string("VOLATILE")))
 2969
 2970        index = self._index
 2971
 2972        seq_props = self._parse_sequence_properties()
 2973        if seq_props:
 2974            return seq_props
 2975
 2976        self._retreat(index)
 2977        return self._parse_key_value_property()
 2978
 2979    def _parse_key_value_property(
 2980        self, parse_value: t.Callable[[], exp.Expr | None] | None = None
 2981    ) -> exp.Property | None:
 2982        index = self._index
 2983        key = self._parse_column()
 2984
 2985        if not self._match(TokenType.EQ):
 2986            self._retreat(index)
 2987            return None
 2988
 2989        # Transform the key to exp.Dot if it's dotted identifiers wrapped in exp.Column or to exp.Var otherwise
 2990        if isinstance(key, exp.Column):
 2991            key = key.to_dot() if len(key.parts) > 1 else exp.var(key.name)
 2992
 2993        value = (
 2994            parse_value()
 2995            if parse_value
 2996            else self._parse_bitwise() or self._parse_var(any_token=True)
 2997        )
 2998
 2999        # Transform the value to exp.Var if it was parsed as exp.Column(exp.Identifier())
 3000        if isinstance(value, exp.Column):
 3001            value = exp.var(value.name)
 3002
 3003        return self.expression(exp.Property(this=key, value=value))
 3004
 3005    def _parse_stored(self) -> exp.FileFormatProperty | exp.StorageHandlerProperty:
 3006        if self._match_text_seq("BY"):
 3007            return self.expression(exp.StorageHandlerProperty(this=self._parse_var_or_string()))
 3008
 3009        self._match(TokenType.ALIAS)
 3010        input_format = self._parse_string() if self._match_text_seq("INPUTFORMAT") else None
 3011        output_format = self._parse_string() if self._match_text_seq("OUTPUTFORMAT") else None
 3012
 3013        return self.expression(
 3014            exp.FileFormatProperty(
 3015                this=(
 3016                    self.expression(
 3017                        exp.InputOutputFormat(
 3018                            input_format=input_format, output_format=output_format
 3019                        )
 3020                    )
 3021                    if input_format or output_format
 3022                    else self._parse_var_or_string() or self._parse_number() or self._parse_id_var()
 3023                ),
 3024                hive_format=True,
 3025            )
 3026        )
 3027
 3028    def _parse_unquoted_field(self) -> exp.Expr | None:
 3029        field = self._parse_field()
 3030        if isinstance(field, exp.Identifier) and not field.quoted:
 3031            field = exp.var(field)
 3032
 3033        return field
 3034
 3035    def _parse_property_assignment(self, exp_class: type[E], **kwargs: t.Any) -> E:
 3036        self._match(TokenType.EQ)
 3037        self._match(TokenType.ALIAS)
 3038
 3039        return self.expression(exp_class(this=self._parse_unquoted_field(), **kwargs))
 3040
 3041    def _parse_properties(self, before: bool | None = None) -> exp.Properties | None:
 3042        properties = []
 3043        while True:
 3044            if before:
 3045                prop = self._parse_property_before()
 3046            else:
 3047                prop = self._parse_property()
 3048            if not prop:
 3049                break
 3050            for p in ensure_list(prop):
 3051                properties.append(p)
 3052
 3053            self._match(TokenType.COMMA, expression=properties[-1])
 3054
 3055        if properties:
 3056            return self.expression(exp.Properties(expressions=properties))
 3057
 3058        return None
 3059
 3060    def _parse_fallback(self, no: bool = False) -> exp.FallbackProperty:
 3061        return self.expression(
 3062            exp.FallbackProperty(no=no, protection=self._match_text_seq("PROTECTION"))
 3063        )
 3064
 3065    def _parse_sql_security(self) -> exp.SqlSecurityProperty:
 3066        return self.expression(
 3067            exp.SqlSecurityProperty(
 3068                this=self._match_texts(self.SECURITY_PROPERTY_KEYWORDS) and self._prev.text.upper()
 3069            )
 3070        )
 3071
 3072    def _parse_settings_property(self) -> exp.SettingsProperty:
 3073        return self.expression(
 3074            exp.SettingsProperty(expressions=self._parse_csv(self._parse_assignment))
 3075        )
 3076
 3077    def _parse_called_on_null_input_property(self) -> exp.CalledOnNullInputProperty | None:
 3078        if not self._match_text_seq("ON", "NULL", "INPUT"):
 3079            self._retreat(self._index - 1)
 3080            return None
 3081
 3082        return self.expression(exp.CalledOnNullInputProperty())
 3083
 3084    def _parse_volatile_property(self) -> exp.VolatileProperty | exp.StabilityProperty:
 3085        if self._index >= 2:
 3086            pre_volatile_token = self._tokens[self._index - 2]
 3087        else:
 3088            pre_volatile_token = None
 3089
 3090        if pre_volatile_token and pre_volatile_token.token_type in self.PRE_VOLATILE_TOKENS:
 3091            return exp.VolatileProperty()
 3092
 3093        return self.expression(exp.StabilityProperty(this=exp.Literal.string("VOLATILE")))
 3094
 3095    def _parse_retention_period(self) -> exp.Var:
 3096        # Parse TSQL's HISTORY_RETENTION_PERIOD: {INFINITE | <number> DAY | DAYS | MONTH ...}
 3097        number = self._parse_number()
 3098        number_str = f"{number} " if number else ""
 3099        unit = self._parse_var(any_token=True)
 3100        return exp.var(f"{number_str}{unit}")
 3101
 3102    def _parse_system_versioning_property(
 3103        self, with_: bool = False
 3104    ) -> exp.WithSystemVersioningProperty:
 3105        self._match(TokenType.EQ)
 3106        prop = self.expression(exp.WithSystemVersioningProperty(on=True, with_=with_))
 3107
 3108        if self._match_text_seq("OFF"):
 3109            prop.set("on", False)
 3110            return prop
 3111
 3112        self._match(TokenType.ON)
 3113        if self._match(TokenType.L_PAREN):
 3114            while self._curr and not self._match(TokenType.R_PAREN):
 3115                if self._match_text_seq("HISTORY_TABLE", "="):
 3116                    prop.set("this", self._parse_table_parts())
 3117                elif self._match_text_seq("DATA_CONSISTENCY_CHECK", "="):
 3118                    prop.set("data_consistency", self._advance_any() and self._prev.text.upper())
 3119                elif self._match_text_seq("HISTORY_RETENTION_PERIOD", "="):
 3120                    prop.set("retention_period", self._parse_retention_period())
 3121
 3122                self._match(TokenType.COMMA)
 3123
 3124        return prop
 3125
 3126    def _parse_data_deletion_property(self) -> exp.DataDeletionProperty:
 3127        self._match(TokenType.EQ)
 3128        on = self._match_text_seq("ON") or not self._match_text_seq("OFF")
 3129        prop = self.expression(exp.DataDeletionProperty(on=on))
 3130
 3131        if self._match(TokenType.L_PAREN):
 3132            while self._curr and not self._match(TokenType.R_PAREN):
 3133                if self._match_text_seq("FILTER_COLUMN", "="):
 3134                    prop.set("filter_column", self._parse_column())
 3135                elif self._match_text_seq("RETENTION_PERIOD", "="):
 3136                    prop.set("retention_period", self._parse_retention_period())
 3137
 3138                self._match(TokenType.COMMA)
 3139
 3140        return prop
 3141
 3142    def _parse_distributed_property(self) -> exp.DistributedByProperty:
 3143        kind = "HASH"
 3144        expressions: list[exp.Expr] | None = None
 3145        if self._match_text_seq("BY", "HASH"):
 3146            expressions = self._parse_wrapped_csv(self._parse_id_var)
 3147        elif self._match_text_seq("BY", "RANDOM"):
 3148            kind = "RANDOM"
 3149
 3150        # If the BUCKETS keyword is not present, the number of buckets is AUTO
 3151        buckets: exp.Expr | None = None
 3152        if self._match_text_seq("BUCKETS") and not self._match_text_seq("AUTO"):
 3153            buckets = self._parse_number()
 3154
 3155        return self.expression(
 3156            exp.DistributedByProperty(
 3157                expressions=expressions, kind=kind, buckets=buckets, order=self._parse_order()
 3158            )
 3159        )
 3160
 3161    def _parse_composite_key_property(self, expr_type: type[E]) -> E:
 3162        self._match_text_seq("KEY")
 3163        expressions = self._parse_wrapped_id_vars()
 3164        return self.expression(expr_type(expressions=expressions))
 3165
 3166    def _parse_with_property(self) -> exp.Expr | None | list[exp.Expr]:
 3167        if self._match_text_seq("(", "SYSTEM_VERSIONING"):
 3168            prop = self._parse_system_versioning_property(with_=True)
 3169            self._match_r_paren()
 3170            return prop
 3171
 3172        if self._match(TokenType.L_PAREN, advance=False):
 3173            result: list[exp.Expr] = []
 3174            for i in self._parse_wrapped_properties():
 3175                result.extend(i) if isinstance(i, list) else result.append(i)
 3176            return result
 3177
 3178        if self._match_text_seq("JOURNAL"):
 3179            return self._parse_withjournaltable()
 3180
 3181        if self._match_texts(self.VIEW_ATTRIBUTES):
 3182            return self.expression(exp.ViewAttributeProperty(this=self._prev.text.upper()))
 3183
 3184        if self._match_text_seq("DATA"):
 3185            return self._parse_withdata(no=False)
 3186        elif self._match_text_seq("NO", "DATA"):
 3187            return self._parse_withdata(no=True)
 3188
 3189        if self._match(TokenType.SERDE_PROPERTIES, advance=False):
 3190            return self._parse_serde_properties(with_=True)
 3191
 3192        if self._match(TokenType.SCHEMA):
 3193            return self.expression(
 3194                exp.WithSchemaBindingProperty(
 3195                    this=self._parse_var_from_options(self.SCHEMA_BINDING_OPTIONS)
 3196                )
 3197            )
 3198
 3199        if self._match_texts(self.PROCEDURE_OPTIONS, advance=False):
 3200            return self.expression(
 3201                exp.WithProcedureOptions(expressions=self._parse_csv(self._parse_procedure_option))
 3202            )
 3203
 3204        if not self._next:
 3205            return None
 3206
 3207        return self._parse_withisolatedloading()
 3208
 3209    def _parse_procedure_option(self) -> exp.Expr | None:
 3210        if self._match_text_seq("EXECUTE", "AS"):
 3211            return self.expression(
 3212                exp.ExecuteAsProperty(
 3213                    this=self._parse_var_from_options(
 3214                        self.EXECUTE_AS_OPTIONS, raise_unmatched=False
 3215                    )
 3216                    or self._parse_string()
 3217                )
 3218            )
 3219
 3220        return self._parse_var_from_options(self.PROCEDURE_OPTIONS)
 3221
 3222    # https://dev.mysql.com/doc/refman/8.0/en/create-view.html
 3223    def _parse_definer(self) -> exp.DefinerProperty | None:
 3224        self._match(TokenType.EQ)
 3225
 3226        user = self._parse_id_var()
 3227        self._match(TokenType.PARAMETER)
 3228        host = self._parse_id_var() or (self._match(TokenType.MOD) and self._prev.text)
 3229
 3230        if not user or not host:
 3231            return None
 3232
 3233        return exp.DefinerProperty(this=f"{user}@{host}")
 3234
 3235    def _parse_withjournaltable(self) -> exp.WithJournalTableProperty:
 3236        self._match(TokenType.TABLE)
 3237        self._match(TokenType.EQ)
 3238        return self.expression(exp.WithJournalTableProperty(this=self._parse_table_parts()))
 3239
 3240    def _parse_log(self, no: bool = False) -> exp.LogProperty:
 3241        return self.expression(exp.LogProperty(no=no))
 3242
 3243    def _parse_journal(self, **kwargs) -> exp.JournalProperty:
 3244        return self.expression(exp.JournalProperty(**kwargs))
 3245
 3246    def _parse_checksum(self) -> exp.ChecksumProperty:
 3247        self._match(TokenType.EQ)
 3248
 3249        on = None
 3250        if self._match(TokenType.ON):
 3251            on = True
 3252        elif self._match_text_seq("OFF"):
 3253            on = False
 3254
 3255        return self.expression(exp.ChecksumProperty(on=on, default=self._match(TokenType.DEFAULT)))
 3256
 3257    def _parse_cluster(self) -> exp.Cluster:
 3258        self._match(TokenType.CLUSTER_BY)
 3259        return self.expression(
 3260            exp.Cluster(
 3261                expressions=self._parse_csv(self._parse_column),
 3262            )
 3263        )
 3264
 3265    def _parse_cluster_property(self) -> exp.ClusterProperty:
 3266        return self.expression(
 3267            exp.ClusterProperty(
 3268                expressions=self._parse_wrapped_csv(self._parse_column),
 3269            )
 3270        )
 3271
 3272    def _parse_clustered_by(self) -> exp.ClusteredByProperty:
 3273        self._match_text_seq("BY")
 3274
 3275        self._match_l_paren()
 3276        expressions = self._parse_csv(self._parse_column)
 3277        self._match_r_paren()
 3278
 3279        if self._match_text_seq("SORTED", "BY"):
 3280            self._match_l_paren()
 3281            sorted_by = self._parse_csv(self._parse_ordered)
 3282            self._match_r_paren()
 3283        else:
 3284            sorted_by = None
 3285
 3286        self._match(TokenType.INTO)
 3287        buckets = self._parse_number()
 3288        self._match_text_seq("BUCKETS")
 3289
 3290        return self.expression(
 3291            exp.ClusteredByProperty(expressions=expressions, sorted_by=sorted_by, buckets=buckets)
 3292        )
 3293
 3294    def _parse_copy_property(self) -> exp.CopyGrantsProperty | None:
 3295        if not self._match_text_seq("GRANTS"):
 3296            self._retreat(self._index - 1)
 3297            return None
 3298
 3299        return self.expression(exp.CopyGrantsProperty())
 3300
 3301    def _parse_freespace(self) -> exp.FreespaceProperty:
 3302        self._match(TokenType.EQ)
 3303        return self.expression(
 3304            exp.FreespaceProperty(this=self._parse_number(), percent=self._match(TokenType.PERCENT))
 3305        )
 3306
 3307    def _parse_mergeblockratio(
 3308        self, no: bool = False, default: bool = False
 3309    ) -> exp.MergeBlockRatioProperty:
 3310        if self._match(TokenType.EQ):
 3311            return self.expression(
 3312                exp.MergeBlockRatioProperty(
 3313                    this=self._parse_number(), percent=self._match(TokenType.PERCENT)
 3314                )
 3315            )
 3316
 3317        return self.expression(exp.MergeBlockRatioProperty(no=no, default=default))
 3318
 3319    def _parse_datablocksize(
 3320        self,
 3321        default: bool | None = None,
 3322        minimum: bool | None = None,
 3323        maximum: bool | None = None,
 3324    ) -> exp.DataBlocksizeProperty:
 3325        self._match(TokenType.EQ)
 3326        size = self._parse_number()
 3327
 3328        units = None
 3329        if self._match_texts(("BYTES", "KBYTES", "KILOBYTES")):
 3330            units = self._prev.text
 3331
 3332        return self.expression(
 3333            exp.DataBlocksizeProperty(
 3334                size=size, units=units, default=default, minimum=minimum, maximum=maximum
 3335            )
 3336        )
 3337
 3338    def _parse_blockcompression(self) -> exp.BlockCompressionProperty:
 3339        self._match(TokenType.EQ)
 3340        always = self._match_text_seq("ALWAYS")
 3341        manual = self._match_text_seq("MANUAL")
 3342        never = self._match_text_seq("NEVER")
 3343        default = self._match_text_seq("DEFAULT")
 3344
 3345        autotemp = None
 3346        if self._match_text_seq("AUTOTEMP"):
 3347            autotemp = self._parse_schema()
 3348
 3349        return self.expression(
 3350            exp.BlockCompressionProperty(
 3351                always=always, manual=manual, never=never, default=default, autotemp=autotemp
 3352            )
 3353        )
 3354
 3355    def _parse_withisolatedloading(self) -> exp.IsolatedLoadingProperty | None:
 3356        index = self._index
 3357        no = self._match_text_seq("NO")
 3358        concurrent = self._match_text_seq("CONCURRENT")
 3359
 3360        if not self._match_text_seq("ISOLATED", "LOADING"):
 3361            self._retreat(index)
 3362            return None
 3363
 3364        target = self._parse_var_from_options(self.ISOLATED_LOADING_OPTIONS, raise_unmatched=False)
 3365        return self.expression(
 3366            exp.IsolatedLoadingProperty(no=no, concurrent=concurrent, target=target)
 3367        )
 3368
 3369    def _parse_locking(self) -> exp.LockingProperty:
 3370        if self._match(TokenType.TABLE):
 3371            kind = "TABLE"
 3372        elif self._match(TokenType.VIEW):
 3373            kind = "VIEW"
 3374        elif self._match(TokenType.ROW):
 3375            kind = "ROW"
 3376        elif self._match_text_seq("DATABASE"):
 3377            kind = "DATABASE"
 3378        else:
 3379            kind = None
 3380
 3381        if kind in ("DATABASE", "TABLE", "VIEW"):
 3382            this = self._parse_table_parts()
 3383        else:
 3384            this = None
 3385
 3386        if self._match(TokenType.FOR):
 3387            for_or_in = "FOR"
 3388        elif self._match(TokenType.IN):
 3389            for_or_in = "IN"
 3390        else:
 3391            for_or_in = None
 3392
 3393        if self._match_text_seq("ACCESS"):
 3394            lock_type = "ACCESS"
 3395        elif self._match_texts(("EXCL", "EXCLUSIVE")):
 3396            lock_type = "EXCLUSIVE"
 3397        elif self._match_text_seq("SHARE"):
 3398            lock_type = "SHARE"
 3399        elif self._match_text_seq("READ"):
 3400            lock_type = "READ"
 3401        elif self._match_text_seq("WRITE"):
 3402            lock_type = "WRITE"
 3403        elif self._match_text_seq("CHECKSUM"):
 3404            lock_type = "CHECKSUM"
 3405        else:
 3406            lock_type = None
 3407
 3408        override = self._match_text_seq("OVERRIDE")
 3409
 3410        return self.expression(
 3411            exp.LockingProperty(
 3412                this=this, kind=kind, for_or_in=for_or_in, lock_type=lock_type, override=override
 3413            )
 3414        )
 3415
 3416    def _parse_partition_by(self) -> list[exp.Expr]:
 3417        if self._match(TokenType.PARTITION_BY):
 3418            return self._parse_csv(self._parse_disjunction)
 3419        return []
 3420
 3421    def _parse_partition_bound_spec(self) -> exp.PartitionBoundSpec:
 3422        def _parse_partition_bound_expr() -> exp.Expr | None:
 3423            if self._match_text_seq("MINVALUE"):
 3424                return exp.var("MINVALUE")
 3425            if self._match_text_seq("MAXVALUE"):
 3426                return exp.var("MAXVALUE")
 3427            return self._parse_bitwise()
 3428
 3429        this: exp.Expr | list[exp.Expr] | None = None
 3430        expression = None
 3431        from_expressions = None
 3432        to_expressions = None
 3433
 3434        if self._match(TokenType.IN):
 3435            this = self._parse_wrapped_csv(self._parse_bitwise)
 3436        elif self._match(TokenType.FROM):
 3437            from_expressions = self._parse_wrapped_csv(_parse_partition_bound_expr)
 3438            self._match_text_seq("TO")
 3439            to_expressions = self._parse_wrapped_csv(_parse_partition_bound_expr)
 3440        elif self._match_text_seq("WITH", "(", "MODULUS"):
 3441            this = self._parse_number()
 3442            self._match_text_seq(",", "REMAINDER")
 3443            expression = self._parse_number()
 3444            self._match_r_paren()
 3445        else:
 3446            self.raise_error("Failed to parse partition bound spec.")
 3447
 3448        return self.expression(
 3449            exp.PartitionBoundSpec(
 3450                this=this,
 3451                expression=expression,
 3452                from_expressions=from_expressions,
 3453                to_expressions=to_expressions,
 3454            )
 3455        )
 3456
 3457    # https://www.postgresql.org/docs/current/sql-createtable.html
 3458    def _parse_partitioned_of(self) -> exp.PartitionedOfProperty | None:
 3459        if not self._match_text_seq("OF"):
 3460            self._retreat(self._index - 1)
 3461            return None
 3462
 3463        this = self._parse_table(schema=True)
 3464
 3465        if self._match(TokenType.DEFAULT):
 3466            expression: exp.Var | exp.PartitionBoundSpec = exp.var("DEFAULT")
 3467        elif self._match_text_seq("FOR", "VALUES"):
 3468            expression = self._parse_partition_bound_spec()
 3469        else:
 3470            self.raise_error("Expecting either DEFAULT or FOR VALUES clause.")
 3471
 3472        return self.expression(exp.PartitionedOfProperty(this=this, expression=expression))
 3473
 3474    def _parse_partitioned_by(self) -> exp.PartitionedByProperty:
 3475        self._match(TokenType.EQ)
 3476        return self.expression(
 3477            exp.PartitionedByProperty(
 3478                this=self._parse_schema() or self._parse_bracket(self._parse_field())
 3479            )
 3480        )
 3481
 3482    def _parse_withdata(self, no: bool = False) -> exp.WithDataProperty:
 3483        if self._match_text_seq("AND", "STATISTICS"):
 3484            statistics = True
 3485        elif self._match_text_seq("AND", "NO", "STATISTICS"):
 3486            statistics = False
 3487        else:
 3488            statistics = None
 3489
 3490        return self.expression(exp.WithDataProperty(no=no, statistics=statistics))
 3491
 3492    def _parse_contains_property(self) -> exp.SqlReadWriteProperty | None:
 3493        if self._match_text_seq("SQL"):
 3494            return self.expression(exp.SqlReadWriteProperty(this="CONTAINS SQL"))
 3495        return None
 3496
 3497    def _parse_modifies_property(self) -> exp.SqlReadWriteProperty | None:
 3498        if self._match_text_seq("SQL", "DATA"):
 3499            return self.expression(exp.SqlReadWriteProperty(this="MODIFIES SQL DATA"))
 3500        return None
 3501
 3502    def _parse_no_property(self) -> exp.Expr | None:
 3503        if self._match_text_seq("PRIMARY", "INDEX"):
 3504            return exp.NoPrimaryIndexProperty()
 3505        if self._match_text_seq("SQL"):
 3506            return self.expression(exp.SqlReadWriteProperty(this="NO SQL"))
 3507        return None
 3508
 3509    def _parse_on_property(self) -> exp.Expr | None:
 3510        if self._match_text_seq("COMMIT", "PRESERVE", "ROWS"):
 3511            return exp.OnCommitProperty()
 3512        if self._match_text_seq("COMMIT", "DELETE", "ROWS"):
 3513            return exp.OnCommitProperty(delete=True)
 3514        return self.expression(exp.OnProperty(this=self._parse_schema(self._parse_id_var())))
 3515
 3516    def _parse_reads_property(self) -> exp.SqlReadWriteProperty | None:
 3517        if self._match_text_seq("SQL", "DATA"):
 3518            return self.expression(exp.SqlReadWriteProperty(this="READS SQL DATA"))
 3519        return None
 3520
 3521    def _parse_distkey(self) -> exp.DistKeyProperty:
 3522        return self.expression(exp.DistKeyProperty(this=self._parse_wrapped(self._parse_id_var)))
 3523
 3524    def _parse_create_like(self) -> exp.LikeProperty | None:
 3525        table = self._parse_table(schema=True)
 3526
 3527        options = []
 3528        while self._match_texts(("INCLUDING", "EXCLUDING")):
 3529            this = self._prev.text.upper()
 3530
 3531            id_var = self._parse_id_var()
 3532            if not id_var:
 3533                return None
 3534
 3535            options.append(
 3536                self.expression(exp.Property(this=this, value=exp.var(id_var.this.upper())))
 3537            )
 3538
 3539        return self.expression(exp.LikeProperty(this=table, expressions=options))
 3540
 3541    def _parse_sortkey(self, compound: bool = False) -> exp.SortKeyProperty:
 3542        return self.expression(
 3543            exp.SortKeyProperty(this=self._parse_wrapped_id_vars(), compound=compound)
 3544        )
 3545
 3546    def _parse_character_set(self, default: bool = False) -> exp.CharacterSetProperty:
 3547        self._match(TokenType.EQ)
 3548        return self.expression(
 3549            exp.CharacterSetProperty(this=self._parse_var_or_string(), default=default)
 3550        )
 3551
 3552    def _parse_remote_with_connection(self) -> exp.RemoteWithConnectionModelProperty:
 3553        self._match_text_seq("WITH", "CONNECTION")
 3554        return self.expression(
 3555            exp.RemoteWithConnectionModelProperty(this=self._parse_table_parts())
 3556        )
 3557
 3558    def _parse_returns(self) -> exp.ReturnsProperty:
 3559        value: exp.Expr | None
 3560        null = None
 3561        is_table = self._match(TokenType.TABLE)
 3562
 3563        if is_table:
 3564            if self._match(TokenType.LT):
 3565                value = self.expression(
 3566                    exp.Schema(this="TABLE", expressions=self._parse_csv(self._parse_struct_types))
 3567                )
 3568                if not self._match(TokenType.GT):
 3569                    self.raise_error("Expecting >")
 3570            else:
 3571                value = self._parse_schema(exp.var("TABLE"))
 3572        elif self._match_text_seq("NULL", "ON", "NULL", "INPUT"):
 3573            null = True
 3574            value = None
 3575        else:
 3576            value = self._parse_types()
 3577
 3578        return self.expression(exp.ReturnsProperty(this=value, is_table=is_table, null=null))
 3579
 3580    def _parse_describe(self) -> exp.Describe:
 3581        kind = self._prev.text if self._match_set(self.CREATABLES) else None
 3582        style: str | None = (
 3583            self._prev.text.upper() if self._match_texts(self.DESCRIBE_STYLES) else None
 3584        )
 3585        if self._match(TokenType.DOT):
 3586            style = None
 3587            self._retreat(self._index - 2)
 3588
 3589        format = self._parse_property() if self._match(TokenType.FORMAT, advance=False) else None
 3590
 3591        if self._match_set(self.STATEMENT_PARSERS, advance=False):
 3592            this = self._parse_statement()
 3593        else:
 3594            this = self._parse_table(schema=True)
 3595
 3596        properties = self._parse_properties()
 3597        expressions = properties.expressions if properties else None
 3598        partition = self._parse_partition()
 3599        return self.expression(
 3600            exp.Describe(
 3601                this=this,
 3602                style=style,
 3603                kind=kind,
 3604                expressions=expressions,
 3605                partition=partition,
 3606                format=format,
 3607                as_json=self._match_text_seq("AS", "JSON"),
 3608            )
 3609        )
 3610
 3611    def _parse_multitable_inserts(
 3612        self, comments: list[str] | None, kind: str | None = None
 3613    ) -> exp.MultitableInserts:
 3614        kind = kind or self._prev.text.upper()
 3615        expressions = []
 3616
 3617        def parse_conditional_insert() -> exp.ConditionalInsert | None:
 3618            if self._match(TokenType.WHEN):
 3619                expression = self._parse_disjunction()
 3620                self._match(TokenType.THEN)
 3621            else:
 3622                expression = None
 3623
 3624            else_ = self._match(TokenType.ELSE)
 3625
 3626            if not self._match(TokenType.INTO):
 3627                return None
 3628
 3629            return self.expression(
 3630                exp.ConditionalInsert(
 3631                    this=self.expression(
 3632                        exp.Insert(
 3633                            this=self._parse_table(schema=True),
 3634                            expression=self._parse_derived_table_values(),
 3635                        )
 3636                    ),
 3637                    expression=expression,
 3638                    else_=else_,
 3639                )
 3640            )
 3641
 3642        expression = parse_conditional_insert()
 3643        while expression is not None:
 3644            expressions.append(expression)
 3645            expression = parse_conditional_insert()
 3646
 3647        return self.expression(
 3648            exp.MultitableInserts(kind=kind, expressions=expressions, source=self._parse_table()),
 3649            comments=comments,
 3650        )
 3651
 3652    def _parse_insert(self) -> exp.Insert | exp.MultitableInserts:
 3653        comments: list[str] = []
 3654        hint = self._parse_hint()
 3655        overwrite = self._match(TokenType.OVERWRITE)
 3656        ignore = self._match(TokenType.IGNORE)
 3657        local = self._match_text_seq("LOCAL")
 3658        alternative = None
 3659        is_function = None
 3660
 3661        if self._match_text_seq("DIRECTORY"):
 3662            this: exp.Expr | None = self.expression(
 3663                exp.Directory(
 3664                    this=self._parse_var_or_string(),
 3665                    local=local,
 3666                    row_format=self._parse_row_format(match_row=True),
 3667                )
 3668            )
 3669        else:
 3670            if self._match_set((TokenType.FIRST, TokenType.ALL)):
 3671                comments += ensure_list(self._prev_comments)
 3672                return self._parse_multitable_inserts(comments)
 3673
 3674            # Oracle's conditional form defaults to ALL when the keyword is omitted
 3675            if self._match(TokenType.WHEN, advance=False):
 3676                return self._parse_multitable_inserts(comments, kind="ALL")
 3677
 3678            if self._match(TokenType.OR):
 3679                alternative = self._match_texts(self.INSERT_ALTERNATIVES) and self._prev.text
 3680
 3681            self._match(TokenType.INTO)
 3682            comments += ensure_list(self._prev_comments)
 3683            self._match(TokenType.TABLE)
 3684            is_function = self._match(TokenType.FUNCTION)
 3685
 3686            this = self._parse_function() if is_function else self._parse_insert_table()
 3687
 3688        # MySQL's INSERT ... SET is normalized into the INSERT ... (cols) VALUES (vals) variant
 3689        set_values = None
 3690        if self._match(TokenType.SET):
 3691            columns = []
 3692            values = []
 3693
 3694            def _parse_set_assignment() -> exp.Expr | None:
 3695                target = self._parse_column()
 3696                if isinstance(target, exp.Column) and self._match(TokenType.EQ):
 3697                    if self.dialect.SUPPORTS_VALUES_DEFAULT and self._match(TokenType.DEFAULT):
 3698                        value: exp.Expr | None = exp.var(self._prev.text.upper())
 3699                    else:
 3700                        value = self._parse_disjunction()
 3701
 3702                    if value:
 3703                        columns.append(target.this)
 3704                        values.append(value)
 3705                        return value
 3706
 3707                self.raise_error("Expected column assignment in INSERT ... SET")
 3708                return None
 3709
 3710            self._parse_csv(_parse_set_assignment)
 3711
 3712            this = self.expression(exp.Schema(this=this, expressions=columns))
 3713            set_values = self.expression(
 3714                exp.Values(
 3715                    expressions=[exp.Tuple(expressions=values)],
 3716                    alias=self._parse_table_alias(),
 3717                )
 3718            )
 3719
 3720        returning = self._parse_returning()  # TSQL allows RETURNING before source
 3721
 3722        stored = self._match_text_seq("STORED") and self._parse_stored()
 3723        by_name = self._match_text_seq("BY", "NAME")
 3724        exists = self._parse_exists()
 3725        replace_where = None
 3726        replace_using = None
 3727
 3728        if self._match(TokenType.REPLACE):
 3729            if self._match(TokenType.WHERE):
 3730                replace_where = self._parse_disjunction()
 3731            elif self._match(TokenType.USING):
 3732                replace_using = self._parse_using_identifiers()
 3733
 3734        return self.expression(
 3735            exp.Insert(
 3736                hint=hint,
 3737                is_function=is_function,
 3738                this=this,
 3739                stored=stored,
 3740                by_name=by_name,
 3741                exists=exists,
 3742                where=replace_where,
 3743                using=replace_using,
 3744                partition=self._match(TokenType.PARTITION_BY) and self._parse_partitioned_by(),
 3745                settings=self._match_text_seq("SETTINGS") and self._parse_settings_property(),
 3746                default=self._match_text_seq("DEFAULT", "VALUES"),
 3747                expression=set_values
 3748                or self._parse_derived_table_values(allow_value_synonym=True)
 3749                or self._parse_ddl_select(),
 3750                conflict=self._parse_on_conflict(),
 3751                returning=returning or self._parse_returning(),
 3752                overwrite=overwrite,
 3753                alternative=alternative,
 3754                ignore=ignore,
 3755                source=self._match(TokenType.TABLE) and self._parse_table(),
 3756            ),
 3757            comments=comments,
 3758        )
 3759
 3760    def _parse_insert_table(self) -> exp.Expr | None:
 3761        this = self._parse_table(schema=True, parse_partition=True)
 3762        if isinstance(this, exp.Table) and self._match(TokenType.ALIAS, advance=False):
 3763            this.set("alias", self._parse_table_alias())
 3764        return this
 3765
 3766    def _parse_kill(self) -> exp.Kill:
 3767        kind = exp.var(self._prev.text) if self._match_texts(("CONNECTION", "QUERY")) else None
 3768
 3769        return self.expression(exp.Kill(this=self._parse_primary(), kind=kind))
 3770
 3771    def _parse_on_conflict(self) -> exp.OnConflict | None:
 3772        conflict = self._match_text_seq("ON", "CONFLICT")
 3773        duplicate = self._match_text_seq("ON", "DUPLICATE", "KEY")
 3774
 3775        if not conflict and not duplicate:
 3776            return None
 3777
 3778        conflict_keys = None
 3779        constraint = None
 3780
 3781        if conflict:
 3782            if self._match_text_seq("ON", "CONSTRAINT"):
 3783                constraint = self._parse_id_var()
 3784            elif self._match(TokenType.L_PAREN):
 3785                conflict_keys = self._parse_csv(self._parse_indexed_column)
 3786                self._match_r_paren()
 3787
 3788        index_predicate = self._parse_where()
 3789
 3790        action = self._parse_var_from_options(self.CONFLICT_ACTIONS)
 3791        if self._prev.token_type == TokenType.UPDATE:
 3792            self._match(TokenType.SET)
 3793            expressions = self._parse_csv(self._parse_update_assignment)
 3794        else:
 3795            expressions = None
 3796
 3797        return self.expression(
 3798            exp.OnConflict(
 3799                duplicate=duplicate,
 3800                expressions=expressions,
 3801                action=action,
 3802                conflict_keys=conflict_keys,
 3803                index_predicate=index_predicate,
 3804                constraint=constraint,
 3805                where=self._parse_where(),
 3806            )
 3807        )
 3808
 3809    def _parse_returning(self) -> exp.Returning | None:
 3810        if not self._match(TokenType.RETURNING):
 3811            return None
 3812        return self.expression(
 3813            exp.Returning(
 3814                expressions=self._parse_csv(self._parse_expression),
 3815                into=self._match(TokenType.INTO) and self._parse_table_part(),
 3816            )
 3817        )
 3818
 3819    def _parse_row(self) -> exp.RowFormatSerdeProperty | exp.RowFormatDelimitedProperty | None:
 3820        if not self._match(TokenType.FORMAT):
 3821            return None
 3822        return self._parse_row_format()
 3823
 3824    def _parse_serde_properties(self, with_: bool = False) -> exp.SerdeProperties | None:
 3825        index = self._index
 3826        with_ = with_ or self._match_text_seq("WITH")
 3827
 3828        if not self._match(TokenType.SERDE_PROPERTIES):
 3829            self._retreat(index)
 3830            return None
 3831        return self.expression(
 3832            exp.SerdeProperties(expressions=self._parse_wrapped_properties(), with_=with_)
 3833        )
 3834
 3835    def _parse_row_format(
 3836        self, match_row: bool = False
 3837    ) -> exp.RowFormatSerdeProperty | exp.RowFormatDelimitedProperty | None:
 3838        if match_row and not self._match_pair(TokenType.ROW, TokenType.FORMAT):
 3839            return None
 3840
 3841        if self._match_text_seq("SERDE"):
 3842            this = self._parse_string()
 3843
 3844            serde_properties = self._parse_serde_properties()
 3845
 3846            return self.expression(
 3847                exp.RowFormatSerdeProperty(this=this, serde_properties=serde_properties)
 3848            )
 3849
 3850        self._match_text_seq("DELIMITED")
 3851
 3852        kwargs = {}
 3853
 3854        if self._match_text_seq("FIELDS", "TERMINATED", "BY"):
 3855            kwargs["fields"] = self._parse_string()
 3856            if self._match_text_seq("ESCAPED", "BY"):
 3857                kwargs["escaped"] = self._parse_string()
 3858        if self._match_text_seq("COLLECTION", "ITEMS", "TERMINATED", "BY"):
 3859            kwargs["collection_items"] = self._parse_string()
 3860        if self._match_text_seq("MAP", "KEYS", "TERMINATED", "BY"):
 3861            kwargs["map_keys"] = self._parse_string()
 3862        if self._match_text_seq("LINES", "TERMINATED", "BY"):
 3863            kwargs["lines"] = self._parse_string()
 3864        if self._match_text_seq("NULL", "DEFINED", "AS"):
 3865            kwargs["null"] = self._parse_string()
 3866
 3867        return self.expression(exp.RowFormatDelimitedProperty(**kwargs))  # type: ignore
 3868
 3869    def _parse_load(self) -> exp.LoadData | exp.Command:
 3870        if self._match_text_seq("DATA"):
 3871            local = self._match_text_seq("LOCAL")
 3872            self._match_text_seq("INPATH")
 3873            inpath = self._parse_string()
 3874            overwrite = self._match(TokenType.OVERWRITE)
 3875            temp: bool | None = None
 3876            if self._match(TokenType.INTO):
 3877                temp = self._match(TokenType.TEMPORARY)
 3878                self._match(TokenType.TABLE)
 3879
 3880            return self.expression(
 3881                exp.LoadData(
 3882                    this=self._parse_table(schema=True),
 3883                    local=local,
 3884                    overwrite=overwrite,
 3885                    temp=temp,
 3886                    inpath=inpath,
 3887                    files=self._match_text_seq("FROM", "FILES")
 3888                    and exp.Properties(expressions=self._parse_wrapped_properties()),
 3889                    partition=self._parse_partition(),
 3890                    input_format=self._match_text_seq("INPUTFORMAT") and self._parse_string(),
 3891                    serde=self._match_text_seq("SERDE") and self._parse_string(),
 3892                )
 3893            )
 3894        return self._parse_as_command(self._prev)
 3895
 3896    def _parse_delete(self) -> exp.Delete:
 3897        hint = self._parse_hint()
 3898
 3899        # This handles MySQL's "Multiple-Table Syntax"
 3900        # https://dev.mysql.com/doc/refman/8.0/en/delete.html
 3901        tables = None
 3902        if not self._match(TokenType.FROM, advance=False):
 3903            tables = self._parse_csv(self._parse_table) or None
 3904
 3905        returning = self._parse_returning()
 3906
 3907        return self.expression(
 3908            exp.Delete(
 3909                hint=hint,
 3910                tables=tables,
 3911                this=self._match(TokenType.FROM) and self._parse_table(joins=True),
 3912                using=self._match(TokenType.USING)
 3913                and self._parse_csv(lambda: self._parse_table(joins=True)),
 3914                cluster=self._match(TokenType.ON) and self._parse_on_property(),
 3915                where=self._parse_where(),
 3916                returning=returning or self._parse_returning(),
 3917                order=self._parse_order(),
 3918                limit=self._parse_limit(),
 3919            )
 3920        )
 3921
 3922    def _parse_update_assignment(self) -> exp.Expr | None:
 3923        this = self._parse_comparison()
 3924        if self._match(TokenType.EQ):
 3925            comments = self._prev_comments
 3926            this = self.expression(
 3927                exp.EQ(this=this, expression=self._parse_disjunction()), comments=comments
 3928            )
 3929        return this
 3930
 3931    def _parse_update(self) -> exp.Update:
 3932        hint = self._parse_hint()
 3933        kwargs: dict[str, object] = {
 3934            "hint": hint,
 3935            "this": self._parse_table(joins=True, alias_tokens=self.UPDATE_ALIAS_TOKENS),
 3936        }
 3937        while self._curr:
 3938            if self._match(TokenType.SET):
 3939                kwargs["expressions"] = self._parse_csv(self._parse_update_assignment)
 3940            elif self._match(TokenType.RETURNING, advance=False):
 3941                kwargs["returning"] = self._parse_returning()
 3942            elif self._match(TokenType.FROM, advance=False):
 3943                from_ = self._parse_from(joins=True)
 3944                table = from_.this if from_ else None
 3945                if isinstance(table, exp.Subquery) and self._match(TokenType.JOIN, advance=False):
 3946                    table.set("joins", list(self._parse_joins()) or None)
 3947
 3948                kwargs["from_"] = from_
 3949            elif self._match(TokenType.WHERE, advance=False):
 3950                kwargs["where"] = self._parse_where()
 3951            elif self._match(TokenType.ORDER_BY, advance=False):
 3952                kwargs["order"] = self._parse_order()
 3953            elif self._match(TokenType.LIMIT, advance=False):
 3954                kwargs["limit"] = self._parse_limit()
 3955            else:
 3956                break
 3957
 3958        return self.expression(exp.Update(**kwargs))
 3959
 3960    def _parse_use(self) -> exp.Use:
 3961        return self.expression(
 3962            exp.Use(
 3963                kind=self._parse_var_from_options(self.USABLES, raise_unmatched=False),
 3964                this=self._parse_table(schema=False),
 3965            )
 3966        )
 3967
 3968    def _parse_uncache(self) -> exp.Uncache:
 3969        if not self._match(TokenType.TABLE):
 3970            self.raise_error("Expecting TABLE after UNCACHE")
 3971
 3972        return self.expression(
 3973            exp.Uncache(exists=self._parse_exists(), this=self._parse_table(schema=True))
 3974        )
 3975
 3976    def _parse_cache(self) -> exp.Cache:
 3977        lazy = self._match_text_seq("LAZY")
 3978        self._match(TokenType.TABLE)
 3979        table = self._parse_table(schema=True)
 3980
 3981        options = []
 3982        if self._match_text_seq("OPTIONS"):
 3983            self._match_l_paren()
 3984            k = self._parse_string()
 3985            self._match(TokenType.EQ)
 3986            v = self._parse_string()
 3987            options = [k, v]
 3988            self._match_r_paren()
 3989
 3990        self._match(TokenType.ALIAS)
 3991        return self.expression(
 3992            exp.Cache(
 3993                this=table, lazy=lazy, options=options, expression=self._parse_select(nested=True)
 3994            )
 3995        )
 3996
 3997    def _parse_partition(self) -> exp.Partition | None:
 3998        if not self._match_texts(self.PARTITION_KEYWORDS):
 3999            return None
 4000
 4001        return self.expression(
 4002            exp.Partition(
 4003                subpartition=self._prev.text.upper() == "SUBPARTITION",
 4004                expressions=self._parse_wrapped_csv(self._parse_disjunction),
 4005            )
 4006        )
 4007
 4008    def _parse_value(self, values: bool = True) -> exp.Tuple | None:
 4009        def _parse_value_expression() -> exp.Expr | None:
 4010            if self.dialect.SUPPORTS_VALUES_DEFAULT and self._match(TokenType.DEFAULT):
 4011                return exp.var(self._prev.text.upper())
 4012            return self._parse_expression()
 4013
 4014        if self._match(TokenType.L_PAREN):
 4015            expressions = self._parse_csv(_parse_value_expression)
 4016            self._match_r_paren()
 4017            return self.expression(exp.Tuple(expressions=expressions))
 4018
 4019        # In some dialects we can have VALUES 1, 2 which results in 1 column & 2 rows.
 4020        expression = self._parse_expression()
 4021        if expression:
 4022            return self.expression(exp.Tuple(expressions=[expression]))
 4023        return None
 4024
 4025    def _parse_projections(
 4026        self,
 4027    ) -> tuple[list[exp.Expr], list[exp.Expr] | None]:
 4028        return self._parse_expressions(), None
 4029
 4030    def _parse_wrapped_select(self, table: bool = False) -> exp.Expr | None:
 4031        if self._match_set((TokenType.PIVOT, TokenType.UNPIVOT)):
 4032            this: exp.Expr | None = self._parse_simplified_pivot(
 4033                is_unpivot=self._prev.token_type == TokenType.UNPIVOT
 4034            )
 4035        else:
 4036            # Support parentheses for duckdb FROM-first syntax
 4037            this = (
 4038                self._parse_table(consume_pipe=True)
 4039                if table and not self._match(TokenType.FROM, advance=False)
 4040                else self._parse_select(nested=True, parse_set_operation=False)
 4041            )
 4042
 4043            # Transform exp.Values into a exp.Table to pass through parse_query_modifiers
 4044            # in case a modifier (e.g. join) is following
 4045            if table and isinstance(this, exp.Values) and this.alias:
 4046                alias = this.args["alias"].pop()
 4047                this = exp.Table(this=this, alias=alias)
 4048
 4049            this = self._parse_query_modifiers(self._parse_set_operations(this))
 4050
 4051        return this
 4052
 4053    def _parse_select(
 4054        self,
 4055        nested: bool = False,
 4056        table: bool = False,
 4057        parse_subquery_alias: bool = True,
 4058        parse_set_operation: bool = True,
 4059        consume_pipe: bool = True,
 4060    ) -> exp.Expr | None:
 4061        query = self._parse_select_query(
 4062            nested=nested,
 4063            table=table,
 4064            parse_subquery_alias=parse_subquery_alias,
 4065            parse_set_operation=parse_set_operation,
 4066        )
 4067
 4068        if consume_pipe and self._match(TokenType.PIPE_GT, advance=False):
 4069            if isinstance(query, exp.Query):
 4070                query = self._parse_pipe_syntax_query(query)
 4071                query = query.subquery(copy=False) if query and table else query
 4072
 4073        return query
 4074
 4075    def _parse_select_query(
 4076        self,
 4077        nested: bool = False,
 4078        table: bool = False,
 4079        parse_subquery_alias: bool = True,
 4080        parse_set_operation: bool = True,
 4081    ) -> exp.Expr | None:
 4082        cte = self._parse_with()
 4083
 4084        if cte:
 4085            this = self._parse_statement()
 4086
 4087            if not this:
 4088                self.raise_error("Failed to parse any statement following CTE")
 4089                return cte
 4090
 4091            while isinstance(this, exp.Subquery) and this.is_wrapper:
 4092                this = this.this
 4093
 4094            assert this is not None
 4095            if "with_" in this.arg_types:
 4096                if inner_cte := this.args.get("with_"):
 4097                    cte.set("expressions", cte.expressions + inner_cte.expressions)
 4098                    if inner_cte.args.get("recursive"):
 4099                        cte.set("recursive", True)
 4100                this.set("with_", cte)
 4101            else:
 4102                self.raise_error(f"{this.key} does not support CTE")
 4103                this = cte
 4104
 4105            return this
 4106
 4107        # duckdb supports leading with FROM x
 4108        from_ = (
 4109            self._parse_from(joins=True, consume_pipe=True)
 4110            if self._match(TokenType.FROM, advance=False)
 4111            else None
 4112        )
 4113        laterals = list(iter(self._parse_lateral, None)) if from_ else None
 4114
 4115        # Hive supports multi-table inserts: FROM x INSERT ... SELECT ... [INSERT ... SELECT ...]
 4116        if from_ and self._match(TokenType.INSERT, advance=False):
 4117            source = from_.this
 4118            source.set("laterals", laterals or None)
 4119
 4120            inserts = []
 4121            while self._match(TokenType.INSERT, advance=False):
 4122                inserts.append(self._parse_statement())
 4123
 4124            return self.expression(
 4125                exp.MultitableInserts(expressions=inserts, source=source),
 4126                comments=from_.comments,
 4127            )
 4128
 4129        if self._match(TokenType.SELECT):
 4130            comments = self._prev_comments
 4131
 4132            hint = self._parse_hint()
 4133
 4134            if self._next and not self._next.token_type == TokenType.DOT:
 4135                all_ = self._match(TokenType.ALL)
 4136                matched_distinct = self._match_set(self.DISTINCT_TOKENS)
 4137            else:
 4138                all_, matched_distinct = None, False
 4139
 4140            kind = (
 4141                self._prev.text.upper()
 4142                if self._match(TokenType.ALIAS) and self._match_texts(("STRUCT", "VALUE"))
 4143                else None
 4144            )
 4145
 4146            distinct: exp.Expr | None = (
 4147                self.expression(
 4148                    exp.Distinct(
 4149                        on=self._parse_value(values=False) if self._match(TokenType.ON) else None
 4150                    )
 4151                )
 4152                if matched_distinct
 4153                else None
 4154            )
 4155
 4156            operation_modifiers = []
 4157            while self._curr and self._match_texts(self.OPERATION_MODIFIERS):
 4158                operation_modifiers.append(exp.var(self._prev.text.upper()))
 4159
 4160            limit = self._parse_limit(top=True)
 4161
 4162            # Some dialects (e.g. Redshift, T-SQL) allow SELECT TOP N DISTINCT ...
 4163            if limit and not matched_distinct and not all_:
 4164                matched_distinct = self._match_set(self.DISTINCT_TOKENS)
 4165                if matched_distinct:
 4166                    distinct = self.expression(
 4167                        exp.Distinct(
 4168                            on=self._parse_value(values=False)
 4169                            if self._match(TokenType.ON)
 4170                            else None
 4171                        )
 4172                    )
 4173                else:
 4174                    all_ = self._match(TokenType.ALL)
 4175
 4176            if all_ and distinct:
 4177                self.raise_error("Cannot specify both ALL and DISTINCT after SELECT")
 4178
 4179            projections, exclude = self._parse_projections()
 4180
 4181            this = self.expression(
 4182                exp.Select(
 4183                    kind=kind,
 4184                    hint=hint,
 4185                    distinct=distinct,
 4186                    expressions=projections,
 4187                    limit=limit,
 4188                    exclude=exclude,
 4189                    operation_modifiers=operation_modifiers or None,
 4190                )
 4191            )
 4192            this.comments = comments
 4193
 4194            into = self._parse_into()
 4195            if into:
 4196                this.set("into", into)
 4197
 4198            if not from_:
 4199                from_ = self._parse_from()
 4200
 4201            if from_:
 4202                this.set("from_", from_)
 4203
 4204            this = self._parse_query_modifiers(this)
 4205        elif (table or nested) and self._match(TokenType.L_PAREN):
 4206            comments = self._prev_comments
 4207            this = self._parse_wrapped_select(table=table)
 4208
 4209            if this:
 4210                this.add_comments(comments, prepend=True)
 4211
 4212            # We return early here so that the UNION isn't attached to the subquery by the
 4213            # following call to _parse_set_operations, but instead becomes the parent node
 4214            self._match_r_paren()
 4215            return self._parse_subquery(this, parse_alias=parse_subquery_alias)
 4216        elif self._match(TokenType.VALUES, advance=False):
 4217            this = self._parse_derived_table_values()
 4218        elif from_:
 4219            this = exp.select("*").from_(from_.this, copy=False)
 4220            this = self._parse_query_modifiers(this)
 4221        elif self._match(TokenType.SUMMARIZE):
 4222            table = self._match(TokenType.TABLE)
 4223            this = self._parse_select() or self._parse_string() or self._parse_table()
 4224            return self.expression(exp.Summarize(this=this, table=table))
 4225        elif self._match(TokenType.DESCRIBE):
 4226            this = self._parse_describe()
 4227        else:
 4228            this = None
 4229
 4230        if laterals and isinstance(this, exp.Select):
 4231            this.set("laterals", [*laterals, *(this.args.get("laterals") or [])])
 4232
 4233        return self._parse_set_operations(this) if parse_set_operation else this
 4234
 4235    def _parse_recursive_with_search(self) -> exp.RecursiveWithSearch | None:
 4236        self._match_text_seq("SEARCH")
 4237
 4238        kind = self._match_texts(self.RECURSIVE_CTE_SEARCH_KIND) and self._prev.text.upper()
 4239
 4240        if not kind:
 4241            return None
 4242
 4243        self._match_text_seq("FIRST", "BY")
 4244
 4245        return self.expression(
 4246            exp.RecursiveWithSearch(
 4247                kind=kind,
 4248                this=self._parse_id_var(),
 4249                expression=self._match_text_seq("SET") and self._parse_id_var(),
 4250                to=self._match_text_seq("TO") and self._parse_bitwise(),
 4251                default=self._match(TokenType.DEFAULT) and self._parse_bitwise(),
 4252                using=self._match_text_seq("USING") and self._parse_id_var(),
 4253            )
 4254        )
 4255
 4256    def _parse_with(self, skip_with_token: bool = False) -> exp.With | None:
 4257        if not skip_with_token and not self._match(TokenType.WITH):
 4258            return None
 4259
 4260        comments = self._prev_comments
 4261        recursive = self._match(TokenType.RECURSIVE)
 4262
 4263        last_comments = None
 4264        expressions = []
 4265        udfs = []
 4266        while True:
 4267            cte = self._parse_cte()
 4268            if cte:
 4269                if isinstance(cte, exp.FunctionSpecification):
 4270                    udfs.append(cte)
 4271                else:
 4272                    expressions.append(cte)
 4273
 4274                if last_comments:
 4275                    cte.add_comments(last_comments)
 4276
 4277            if not self._match(TokenType.COMMA) and not self._match(TokenType.WITH):
 4278                break
 4279            else:
 4280                self._match(TokenType.WITH)
 4281                recursive = self._match(TokenType.RECURSIVE) or recursive
 4282
 4283            last_comments = self._prev_comments
 4284
 4285        search = self._parse_recursive_with_search()
 4286
 4287        # A SEARCH clause can be followed by a CYCLE clause
 4288        cycle = (
 4289            self._parse_recursive_with_search()
 4290            if search
 4291            and search.args.get("kind") != "CYCLE"
 4292            and self._match_text_seq("CYCLE", advance=False)
 4293            else None
 4294        )
 4295
 4296        return self.expression(
 4297            exp.With(
 4298                expressions=expressions,
 4299                recursive=recursive or None,
 4300                search=search,
 4301                cycle=cycle,
 4302                udfs=udfs or None,
 4303            ),
 4304            comments=comments,
 4305        )
 4306
 4307    def _parse_cte(self) -> exp.CTE | exp.FunctionSpecification | None:
 4308        index = self._index
 4309
 4310        alias = self._parse_table_alias(self.ID_VAR_TOKENS)
 4311        if not alias or not alias.this:
 4312            self.raise_error("Expected CTE to have alias")
 4313
 4314        key_expressions = (
 4315            self._parse_wrapped_id_vars() if self._match_text_seq("USING", "KEY") else None
 4316        )
 4317
 4318        if not self._match(TokenType.ALIAS) and not self.OPTIONAL_ALIAS_TOKEN_CTE:
 4319            self._retreat(index)
 4320            return None
 4321
 4322        comments = self._prev_comments
 4323
 4324        if self._match_text_seq("NOT", "MATERIALIZED"):
 4325            materialized = False
 4326        elif self._match_text_seq("MATERIALIZED"):
 4327            materialized = True
 4328        else:
 4329            materialized = None
 4330
 4331        cte = self.expression(
 4332            exp.CTE(
 4333                this=self._parse_wrapped(self._parse_statement),
 4334                alias=alias,
 4335                materialized=materialized,
 4336                key_expressions=key_expressions,
 4337            ),
 4338            comments=comments,
 4339        )
 4340
 4341        values = cte.this
 4342        if isinstance(values, exp.Values):
 4343            cte.set("this", self._values_to_select(values))
 4344
 4345        return cte
 4346
 4347    def _values_to_select(self, values: exp.Values) -> exp.Select:
 4348        if values.alias:
 4349            return exp.select("*").from_(values)
 4350        return exp.select("*").from_(exp.alias_(values, "_values", table=True))
 4351
 4352    def _parse_table_alias(
 4353        self, alias_tokens: t.Collection[TokenType] | None = None
 4354    ) -> exp.TableAlias | None:
 4355        # In some dialects, LIMIT and OFFSET can act as both identifiers and keywords (clauses)
 4356        # so this section tries to parse the clause version and if it fails, it treats the token
 4357        # as an identifier (alias)
 4358        if self._can_parse_limit_or_offset():
 4359            return None
 4360
 4361        # START is never treated as an implicit alias when followed by WITH, since that
 4362        # would swallow the beginning of a START WITH ... CONNECT BY clause
 4363        if self._curr.text.upper() == "START" and self._next.text.upper() == "WITH":
 4364            return None
 4365
 4366        any_token = self._match(TokenType.ALIAS)
 4367        alias = (
 4368            self._parse_id_var(any_token=any_token, tokens=alias_tokens or self.TABLE_ALIAS_TOKENS)
 4369            or self._parse_string_as_identifier()
 4370        )
 4371
 4372        index = self._index
 4373        if self._match(TokenType.L_PAREN):
 4374            columns = self._parse_csv(self._parse_function_parameter)
 4375            self._match_r_paren() if columns else self._retreat(index)
 4376        else:
 4377            columns = None
 4378
 4379        if not alias and not columns:
 4380            return None
 4381
 4382        table_alias = self.expression(exp.TableAlias(this=alias, columns=columns))
 4383
 4384        # We bubble up comments from the Identifier to the TableAlias
 4385        if isinstance(alias, exp.Identifier):
 4386            table_alias.add_comments(alias.pop_comments())
 4387
 4388        return table_alias
 4389
 4390    def _parse_subquery(
 4391        self, this: exp.Expr | None, parse_alias: bool = True
 4392    ) -> exp.Subquery | None:
 4393        if not this:
 4394            return None
 4395
 4396        return self.expression(
 4397            exp.Subquery(
 4398                this=this,
 4399                pivots=self._parse_pivots(),
 4400                alias=self._parse_table_alias() if parse_alias else None,
 4401                sample=self._parse_table_sample(),
 4402            )
 4403        )
 4404
 4405    def _implicit_unnests_to_explicit(self, this: E) -> E:
 4406        from sqlglot.optimizer.normalize_identifiers import normalize_identifiers as _norm
 4407
 4408        refs = {_norm(this.args["from_"].this.copy(), dialect=self.dialect).alias_or_name}
 4409        for i, join in enumerate(this.args.get("joins") or []):
 4410            table = join.this
 4411            normalized_table = table.copy()
 4412            normalized_table.meta["maybe_column"] = True
 4413            normalized_table = _norm(normalized_table, dialect=self.dialect)
 4414
 4415            if isinstance(table, exp.Table) and not join.args.get("on"):
 4416                if len(normalized_table.parts) > 1 and normalized_table.parts[0].name in refs:
 4417                    table_as_column = table.to_column()
 4418                    unnest = exp.Unnest(expressions=[table_as_column])
 4419
 4420                    # Table.to_column creates a parent Alias node that we want to convert to
 4421                    # a TableAlias and attach to the Unnest, so it matches the parser's output
 4422                    if isinstance(table.args.get("alias"), exp.TableAlias):
 4423                        table_as_column.replace(table_as_column.this)
 4424                        exp.alias_(unnest, None, table=[table.args["alias"].this], copy=False)
 4425
 4426                    table.replace(unnest)
 4427
 4428            refs.add(normalized_table.alias_or_name)
 4429
 4430        return this
 4431
 4432    @t.overload
 4433    def _parse_query_modifiers(self, this: E) -> E: ...
 4434
 4435    @t.overload
 4436    def _parse_query_modifiers(self, this: None) -> None: ...
 4437
 4438    def _parse_query_modifiers(self, this):
 4439        if isinstance(this, self.MODIFIABLES):
 4440            for join in self._parse_joins():
 4441                this.append("joins", join)
 4442            for lateral in iter(self._parse_lateral, None):
 4443                this.append("laterals", lateral)
 4444
 4445            while True:
 4446                if self._match_set(self.QUERY_MODIFIER_PARSERS, advance=False):
 4447                    modifier_token = self._curr
 4448
 4449                    # Defer LIMIT/FETCH after TOP until a set op is built so it applies to the whole result
 4450                    # e.g., SELECT 1 AS x UNION ALL SELECT TOP 2 2 AS x LIMIT 1 -> limit applies to union
 4451                    if (
 4452                        modifier_token.token_type in (TokenType.LIMIT, TokenType.FETCH)
 4453                        and (limit := this.args.get("limit"))
 4454                        and limit.meta.get("top")
 4455                    ):
 4456                        break
 4457
 4458                    parser = self.QUERY_MODIFIER_PARSERS[modifier_token.token_type]
 4459                    key, expression = parser(self)
 4460
 4461                    if expression:
 4462                        if this.args.get(key):
 4463                            self.raise_error(
 4464                                f"Found multiple '{modifier_token.text.upper()}' clauses",
 4465                                token=modifier_token,
 4466                            )
 4467
 4468                        this.set(key, expression)
 4469                        if key == "limit":
 4470                            offset = expression.args.get("offset")
 4471                            expression.set("offset", None)
 4472
 4473                            if offset:
 4474                                if this.args.get("offset"):
 4475                                    self.raise_error(
 4476                                        "Found multiple 'OFFSET' clauses", token=modifier_token
 4477                                    )
 4478
 4479                                offset = exp.Offset(expression=offset)
 4480                                this.set("offset", offset)
 4481
 4482                                limit_by_expressions = expression.expressions
 4483                                expression.set("expressions", None)
 4484                                offset.set("expressions", limit_by_expressions)
 4485                        continue
 4486
 4487                if self._curr.text.upper() == "START":
 4488                    modifier_token = self._curr
 4489                    connect = self._parse_connect()
 4490                    if connect:
 4491                        if this.args.get("connect"):
 4492                            self.raise_error(
 4493                                "Found multiple 'START WITH' clauses", token=modifier_token
 4494                            )
 4495
 4496                        this.set("connect", connect)
 4497                        continue
 4498                break
 4499
 4500        if self.SUPPORTS_IMPLICIT_UNNEST and this and this.args.get("from_"):
 4501            this = self._implicit_unnests_to_explicit(this)
 4502
 4503        return this
 4504
 4505    def _parse_hint_fallback_to_string(self) -> exp.Hint | None:
 4506        start = self._curr
 4507        while self._curr:
 4508            self._advance()
 4509
 4510        end = self._tokens[self._index - 1]
 4511        return exp.Hint(expressions=[self._find_sql(start, end)])
 4512
 4513    def _parse_hint_function_call(self) -> exp.Expr | None:
 4514        return self._parse_function_call()
 4515
 4516    def _parse_hint_body(self) -> exp.Hint | None:
 4517        start_index = self._index
 4518        should_fallback_to_string = False
 4519
 4520        hints = []
 4521        try:
 4522            for hint in iter(
 4523                lambda: self._parse_csv(
 4524                    lambda: self._parse_hint_function_call() or self._parse_var(upper=True),
 4525                ),
 4526                [],
 4527            ):
 4528                hints.extend(hint)
 4529        except ParseError:
 4530            should_fallback_to_string = True
 4531
 4532        if should_fallback_to_string or self._curr:
 4533            self._retreat(start_index)
 4534            return self._parse_hint_fallback_to_string()
 4535
 4536        return self.expression(exp.Hint(expressions=hints))
 4537
 4538    def _parse_hint(self) -> exp.Hint | None:
 4539        if self._match(TokenType.HINT) and self._prev_comments:
 4540            return exp.maybe_parse(self._prev_comments[0], into=exp.Hint, dialect=self.dialect)
 4541
 4542        return None
 4543
 4544    def _parse_into(self) -> exp.Into | None:
 4545        if not self._match(TokenType.INTO):
 4546            return None
 4547
 4548        temp = self._match(TokenType.TEMPORARY)
 4549        unlogged = self._match_text_seq("UNLOGGED")
 4550        self._match(TokenType.TABLE)
 4551
 4552        return self.expression(
 4553            exp.Into(this=self._parse_table(schema=True), temporary=temp, unlogged=unlogged)
 4554        )
 4555
 4556    def _parse_from(
 4557        self,
 4558        joins: bool = False,
 4559        skip_from_token: bool = False,
 4560        consume_pipe: bool = False,
 4561    ) -> exp.From | None:
 4562        if not skip_from_token and not self._match(TokenType.FROM):
 4563            return None
 4564
 4565        comments = self._prev_comments
 4566        return self.expression(
 4567            exp.From(this=self._parse_table(joins=joins, consume_pipe=consume_pipe)),
 4568            comments=comments,
 4569        )
 4570
 4571    def _parse_match_recognize_measure(self) -> exp.MatchRecognizeMeasure:
 4572        return self.expression(
 4573            exp.MatchRecognizeMeasure(
 4574                window_frame=self._match_texts(("FINAL", "RUNNING")) and self._prev.text.upper(),
 4575                this=self._parse_expression(),
 4576            )
 4577        )
 4578
 4579    def _parse_match_recognize(self) -> exp.MatchRecognize | None:
 4580        if not self._match(TokenType.MATCH_RECOGNIZE):
 4581            return None
 4582
 4583        self._match_l_paren()
 4584
 4585        partition = self._parse_partition_by()
 4586        order = self._parse_order()
 4587
 4588        measures = (
 4589            self._parse_csv(self._parse_match_recognize_measure)
 4590            if self._match_text_seq("MEASURES")
 4591            else None
 4592        )
 4593
 4594        if self._match_text_seq("ONE", "ROW", "PER", "MATCH"):
 4595            rows = exp.var("ONE ROW PER MATCH")
 4596        elif self._match_text_seq("ALL", "ROWS", "PER", "MATCH"):
 4597            text = "ALL ROWS PER MATCH"
 4598            if self._match_text_seq("SHOW", "EMPTY", "MATCHES"):
 4599                text += " SHOW EMPTY MATCHES"
 4600            elif self._match_text_seq("OMIT", "EMPTY", "MATCHES"):
 4601                text += " OMIT EMPTY MATCHES"
 4602            elif self._match_text_seq("WITH", "UNMATCHED", "ROWS"):
 4603                text += " WITH UNMATCHED ROWS"
 4604            rows = exp.var(text)
 4605        else:
 4606            rows = None
 4607
 4608        if self._match_text_seq("AFTER", "MATCH", "SKIP"):
 4609            text = "AFTER MATCH SKIP"
 4610            if self._match_text_seq("PAST", "LAST", "ROW"):
 4611                text += " PAST LAST ROW"
 4612            elif self._match_text_seq("TO", "NEXT", "ROW"):
 4613                text += " TO NEXT ROW"
 4614            elif self._match_text_seq("TO", "FIRST") or self._match_text_seq("TO", "LAST"):
 4615                direction = self._prev.text.upper()
 4616                pattern_var = self._advance_any()
 4617                if not pattern_var:
 4618                    self.raise_error(
 4619                        f"Expecting pattern variable after AFTER MATCH SKIP TO {direction}"
 4620                    )
 4621                text += f" TO {direction} {pattern_var.text if pattern_var else ''}"
 4622            after = exp.var(text)
 4623        else:
 4624            after = None
 4625
 4626        if self._match_text_seq("PATTERN"):
 4627            self._match_l_paren()
 4628
 4629            if not self._curr:
 4630                self.raise_error("Expecting )", self._curr)
 4631
 4632            paren = 1
 4633            start = self._curr
 4634
 4635            while self._curr and paren > 0:
 4636                if self._curr.token_type == TokenType.L_PAREN:
 4637                    paren += 1
 4638                if self._curr.token_type == TokenType.R_PAREN:
 4639                    paren -= 1
 4640
 4641                end = self._prev
 4642                self._advance()
 4643
 4644            if paren > 0:
 4645                self.raise_error("Expecting )", self._curr)
 4646
 4647            pattern = exp.var(self._find_sql(start, end))
 4648        else:
 4649            pattern = None
 4650
 4651        define = (
 4652            self._parse_csv(self._parse_name_as_expression)
 4653            if self._match_text_seq("DEFINE")
 4654            else None
 4655        )
 4656
 4657        self._match_r_paren()
 4658
 4659        return self.expression(
 4660            exp.MatchRecognize(
 4661                partition_by=partition,
 4662                order=order,
 4663                measures=measures,
 4664                rows=rows,
 4665                after=after,
 4666                pattern=pattern,
 4667                define=define,
 4668                alias=self._parse_table_alias(),
 4669            )
 4670        )
 4671
 4672    def _parse_lateral(self) -> exp.Lateral | None:
 4673        cross_apply: bool | None = None
 4674        if self._match_pair(TokenType.CROSS, TokenType.APPLY):
 4675            cross_apply = True
 4676        elif self._match_pair(TokenType.OUTER, TokenType.APPLY):
 4677            cross_apply = False
 4678
 4679        if cross_apply is not None:
 4680            this = self._parse_select(table=True)
 4681            view = None
 4682            outer = None
 4683        elif self._match(TokenType.LATERAL):
 4684            this = self._parse_select(table=True)
 4685            view = self._match(TokenType.VIEW)
 4686            outer = self._match(TokenType.OUTER)
 4687        else:
 4688            return None
 4689
 4690        if not this:
 4691            this = (
 4692                self._parse_unnest()
 4693                or self._parse_function()
 4694                or self._parse_id_var(any_token=False)
 4695            )
 4696
 4697            while self._match(TokenType.DOT):
 4698                this = exp.Dot(
 4699                    this=this,
 4700                    expression=self._parse_function() or self._parse_id_var(any_token=False),
 4701                )
 4702
 4703        ordinality: bool | None = None
 4704
 4705        if view:
 4706            table = self._parse_id_var(any_token=False)
 4707            columns = self._parse_csv(self._parse_id_var) if self._match(TokenType.ALIAS) else []
 4708            table_alias: exp.TableAlias | None = self.expression(
 4709                exp.TableAlias(this=table, columns=columns)
 4710            )
 4711        elif isinstance(this, (exp.Subquery, exp.Unnest)) and this.alias:
 4712            # We move the alias from the lateral's child node to the lateral itself
 4713            table_alias = this.args["alias"].pop()
 4714        else:
 4715            ordinality = self._match_pair(TokenType.WITH, TokenType.ORDINALITY)
 4716            table_alias = self._parse_table_alias()
 4717
 4718        return self.expression(
 4719            exp.Lateral(
 4720                this=this,
 4721                view=view,
 4722                outer=outer,
 4723                alias=table_alias,
 4724                cross_apply=cross_apply,
 4725                ordinality=ordinality,
 4726            )
 4727        )
 4728
 4729    def _parse_stream(self) -> exp.Stream | None:
 4730        index = self._index
 4731        if self._match(TokenType.STREAM):
 4732            if this := self._try_parse(self._parse_table):
 4733                return self.expression(exp.Stream(this=this))
 4734            self._retreat(index)
 4735        return None
 4736
 4737    def _parse_join_parts(
 4738        self,
 4739    ) -> tuple[Token | None, Token | None, Token | None]:
 4740        return (
 4741            self._prev if self._match_set(self.JOIN_METHODS) else None,
 4742            self._prev if self._match_set(self.JOIN_SIDES) else None,
 4743            self._prev if self._match_set(self.JOIN_KINDS) else None,
 4744        )
 4745
 4746    def _parse_using_identifiers(self) -> list[exp.Expr]:
 4747        def _parse_column_as_identifier() -> exp.Expr | None:
 4748            this = self._parse_column()
 4749            if isinstance(this, exp.Column):
 4750                return this.this
 4751            return this
 4752
 4753        return self._parse_wrapped_csv(_parse_column_as_identifier, optional=True)
 4754
 4755    def _parse_join(
 4756        self,
 4757        skip_join_token: bool = False,
 4758        parse_bracket: bool = False,
 4759        alias_tokens: t.Collection[TokenType] | None = None,
 4760    ) -> exp.Join | None:
 4761        if self._match(TokenType.COMMA):
 4762            table = self._try_parse(lambda: self._parse_table(alias_tokens=alias_tokens))
 4763            cross_join = self.expression(exp.Join(this=table)) if table else None
 4764
 4765            if cross_join and self.JOINS_HAVE_EQUAL_PRECEDENCE:
 4766                cross_join.set("kind", "CROSS")
 4767
 4768            return cross_join
 4769
 4770        index = self._index
 4771        method, side, kind = self._parse_join_parts()
 4772        directed = self._match_text_seq("DIRECTED")
 4773        hint = self._prev.text if self._match_texts(self.JOIN_HINTS) else None
 4774        join = self._match(TokenType.JOIN) or (kind and kind.token_type == TokenType.STRAIGHT_JOIN)
 4775        join_comments = self._prev_comments
 4776
 4777        if not skip_join_token and not join:
 4778            self._retreat(index)
 4779            kind = None
 4780            method = None
 4781            side = None
 4782
 4783        outer_apply = self._match_pair(TokenType.OUTER, TokenType.APPLY, False)
 4784        cross_apply = self._match_pair(TokenType.CROSS, TokenType.APPLY, False)
 4785
 4786        if not skip_join_token and not join and not outer_apply and not cross_apply:
 4787            return None
 4788
 4789        kwargs: dict[str, t.Any] = {
 4790            "this": self._parse_table(parse_bracket=parse_bracket, alias_tokens=alias_tokens)
 4791        }
 4792        if kind and kind.token_type == TokenType.ARRAY and self._match(TokenType.COMMA):
 4793            kwargs["expressions"] = self._parse_csv(
 4794                lambda: self._parse_table(parse_bracket=parse_bracket, alias_tokens=alias_tokens)
 4795            )
 4796
 4797        if method:
 4798            kwargs["method"] = method.text.upper()
 4799        if side:
 4800            kwargs["side"] = side.text.upper()
 4801        if kind:
 4802            kwargs["kind"] = kind.text.upper()
 4803        if hint:
 4804            kwargs["hint"] = hint
 4805
 4806        if self._match(TokenType.MATCH_CONDITION):
 4807            kwargs["match_condition"] = self._parse_wrapped(self._parse_comparison)
 4808
 4809        if self._match(TokenType.ON):
 4810            kwargs["on"] = self._parse_disjunction()
 4811        elif self._match(TokenType.USING):
 4812            kwargs["using"] = self._parse_using_identifiers()
 4813        elif (
 4814            not method
 4815            and not (outer_apply or cross_apply)
 4816            and not isinstance(kwargs["this"], exp.Unnest)
 4817            and not (kind and kind.token_type in (TokenType.CROSS, TokenType.ARRAY))
 4818        ):
 4819            index = self._index
 4820            joins: list | None = list(self._parse_joins(alias_tokens=alias_tokens))
 4821
 4822            if joins and self._match(TokenType.ON):
 4823                kwargs["on"] = self._parse_disjunction()
 4824            elif joins and self._match(TokenType.USING):
 4825                kwargs["using"] = self._parse_using_identifiers()
 4826            else:
 4827                joins = None
 4828                self._retreat(index)
 4829
 4830            kwargs["this"].set("joins", joins if joins else None)
 4831
 4832        kwargs["pivots"] = self._parse_pivots()
 4833
 4834        comments = [c for token in (method, side, kind) if token for c in token.comments]
 4835        comments = (join_comments or []) + comments
 4836
 4837        if (
 4838            self.ADD_JOIN_ON_TRUE
 4839            and not kwargs.get("on")
 4840            and not kwargs.get("using")
 4841            and not kwargs.get("method")
 4842            and kwargs.get("kind") in (None, "INNER", "OUTER")
 4843        ):
 4844            kwargs["on"] = exp.true()
 4845
 4846        if directed:
 4847            kwargs["directed"] = directed
 4848
 4849        return self.expression(exp.Join(**kwargs), comments=comments)
 4850
 4851    def _parse_opclass(self) -> exp.Expr | None:
 4852        this = self._parse_disjunction()
 4853
 4854        if self._match_texts(self.OPCLASS_FOLLOW_KEYWORDS, advance=False):
 4855            return this
 4856
 4857        if not self._match_set(self.OPTYPE_FOLLOW_TOKENS, advance=False):
 4858            return self.expression(exp.Opclass(this=this, expression=self._parse_table_parts()))
 4859
 4860        return this
 4861
 4862    def _parse_index_params(self) -> exp.IndexParameters:
 4863        using = self._parse_var(any_token=True) if self._match(TokenType.USING) else None
 4864
 4865        if self._match(TokenType.L_PAREN, advance=False):
 4866            columns = self._parse_wrapped_csv(self._parse_with_operator)
 4867        else:
 4868            columns = None
 4869
 4870        include = self._parse_wrapped_id_vars() if self._match_text_seq("INCLUDE") else None
 4871        partition_by = self._parse_partition_by()
 4872        with_storage = self._match(TokenType.WITH) and self._parse_wrapped_properties()
 4873        tablespace = (
 4874            self._parse_var(any_token=True)
 4875            if self._match_text_seq("USING", "INDEX", "TABLESPACE")
 4876            else None
 4877        )
 4878        where = self._parse_where()
 4879
 4880        on = self._parse_field() if self._match(TokenType.ON) else None
 4881
 4882        return self.expression(
 4883            exp.IndexParameters(
 4884                using=using,
 4885                columns=columns,
 4886                include=include,
 4887                partition_by=partition_by,
 4888                where=where,
 4889                with_storage=with_storage,
 4890                tablespace=tablespace,
 4891                on=on,
 4892            )
 4893        )
 4894
 4895    def _parse_index(
 4896        self, index: exp.Expr | None = None, anonymous: bool = False
 4897    ) -> exp.Index | None:
 4898        if index or anonymous:
 4899            unique = None
 4900            primary = None
 4901            amp = None
 4902
 4903            self._match(TokenType.ON)
 4904            self._match(TokenType.TABLE)  # hive
 4905            table = self._parse_table_parts(schema=True)
 4906        else:
 4907            unique = self._match(TokenType.UNIQUE)
 4908            primary = self._match_text_seq("PRIMARY")
 4909            amp = self._match_text_seq("AMP")
 4910
 4911            if not self._match(TokenType.INDEX):
 4912                return None
 4913
 4914            index = self._parse_id_var()
 4915            table = None
 4916
 4917        params = self._parse_index_params()
 4918
 4919        return self.expression(
 4920            exp.Index(
 4921                this=index, table=table, unique=unique, primary=primary, amp=amp, params=params
 4922            )
 4923        )
 4924
 4925    def _parse_table_hints(self) -> list[exp.Expr] | None:
 4926        hints: list[exp.Expr] = []
 4927        if self._match_pair(TokenType.WITH, TokenType.L_PAREN):
 4928            # https://learn.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-table?view=sql-server-ver16
 4929            hints.append(
 4930                self.expression(
 4931                    exp.WithTableHint(
 4932                        expressions=self._parse_csv(
 4933                            lambda: self._parse_function() or self._parse_var(any_token=True)
 4934                        )
 4935                    )
 4936                )
 4937            )
 4938            self._match_r_paren()
 4939        else:
 4940            # https://dev.mysql.com/doc/refman/8.0/en/index-hints.html
 4941            while self._match_set(self.TABLE_INDEX_HINT_TOKENS):
 4942                hint = exp.IndexTableHint(this=self._prev.text.upper())
 4943
 4944                self._match_set((TokenType.INDEX, TokenType.KEY))
 4945                if self._match(TokenType.FOR):
 4946                    hint.set("target", self._advance_any() and self._prev.text.upper())
 4947
 4948                hint.set("expressions", self._parse_wrapped_id_vars())
 4949                hints.append(hint)
 4950
 4951        return hints or None
 4952
 4953    def _parse_table_part(self, schema: bool = False) -> exp.Expr | None:
 4954        return (
 4955            (not schema and self._parse_function(optional_parens=False))
 4956            or self._parse_id_var(any_token=False)
 4957            or self._parse_string_as_identifier()
 4958            or self._parse_placeholder()
 4959        )
 4960
 4961    def _parse_table_parts_fast(self) -> exp.Table | None:
 4962        index = self._index
 4963        parts: list[exp.Identifier] | None = None
 4964        all_comments: list[str] | None = None
 4965
 4966        while self._match_set(self.IDENTIFIER_TOKENS):
 4967            token = self._prev
 4968            comments = self._prev_comments
 4969
 4970            has_dot = self._match(TokenType.DOT)
 4971            curr_tt = self._curr.token_type
 4972
 4973            if not has_dot:
 4974                if curr_tt in self.TABLE_POSTFIX_TOKENS:
 4975                    self._retreat(index)
 4976                    return None
 4977            elif curr_tt not in self.IDENTIFIER_TOKENS:
 4978                self._retreat(index)
 4979                return None
 4980
 4981            if parts is None:
 4982                parts = []
 4983
 4984            if comments:
 4985                if all_comments is None:
 4986                    all_comments = []
 4987                all_comments.extend(comments)
 4988                self._prev_comments = []
 4989
 4990            parts.append(
 4991                self.expression(
 4992                    exp.Identifier(
 4993                        this=token.text, quoted=token.token_type == TokenType.IDENTIFIER
 4994                    ),
 4995                    token,
 4996                )
 4997            )
 4998
 4999            if not has_dot:
 5000                break
 5001
 5002        if parts is None:
 5003            return None
 5004
 5005        n = len(parts)
 5006
 5007        if n == 1:
 5008            table: exp.Table = exp.Table(this=parts[0])
 5009        elif n == 2:
 5010            table = exp.Table(this=parts[1], db=parts[0])
 5011        elif n >= 3:
 5012            this: exp.Identifier | exp.Dot = parts[2]
 5013            for i in range(3, n):
 5014                this = exp.Dot(this=this, expression=parts[i])
 5015
 5016            table = exp.Table(this=this, db=parts[1], catalog=parts[0])
 5017
 5018        if table is None:
 5019            self._retreat(index)
 5020        elif all_comments:
 5021            table.add_comments(all_comments)
 5022        return table
 5023
 5024    def _parse_table_parts(
 5025        self,
 5026        schema: bool = False,
 5027        is_db_reference: bool = False,
 5028        wildcard: bool = False,
 5029        fast: bool = False,
 5030    ) -> exp.Table | exp.Dot | None:
 5031        if fast:
 5032            return self._parse_table_parts_fast()
 5033
 5034        catalog: exp.Expr | str | None = None
 5035        db: exp.Expr | str | None = None
 5036        table: exp.Expr | str | None = self._parse_table_part(schema=schema)
 5037
 5038        while self._match(TokenType.DOT):
 5039            if catalog:
 5040                # This allows nesting the table in arbitrarily many dot expressions if needed
 5041                table = self.expression(
 5042                    exp.Dot(this=table, expression=self._parse_table_part(schema=schema))
 5043                )
 5044            else:
 5045                catalog = db
 5046                db = table
 5047                # "" used for tsql FROM a..b case
 5048                table = self._parse_table_part(schema=schema) or ""
 5049
 5050        if (
 5051            wildcard
 5052            and self._is_connected()
 5053            and (isinstance(table, exp.Identifier) or not table)
 5054            and self._match(TokenType.STAR)
 5055        ):
 5056            if isinstance(table, exp.Identifier):
 5057                table.args["this"] += "*"
 5058            else:
 5059                table = exp.Identifier(this="*")
 5060
 5061        if is_db_reference:
 5062            catalog = db
 5063            db = table
 5064            table = None
 5065
 5066        if not table and not is_db_reference:
 5067            self.raise_error(f"Expected table name but got {self._curr}")
 5068        if not db and is_db_reference:
 5069            self.raise_error(f"Expected database name but got {self._curr}")
 5070
 5071        table = self.expression(exp.Table(this=table, db=db, catalog=catalog))
 5072
 5073        # Bubble up comments from identifier parts to the Table
 5074        comments = []
 5075        for part in table.parts:
 5076            if part_comments := part.pop_comments():
 5077                comments.extend(part_comments)
 5078        if comments:
 5079            table.add_comments(comments)
 5080
 5081        changes = self._parse_changes()
 5082        if changes:
 5083            table.set("changes", changes)
 5084
 5085        at_before = self._parse_historical_data()
 5086        if at_before:
 5087            table.set("when", at_before)
 5088
 5089        pivots = self._parse_pivots()
 5090        if pivots:
 5091            table.set("pivots", pivots)
 5092
 5093        return table
 5094
 5095    def _parse_table(
 5096        self,
 5097        schema: bool = False,
 5098        joins: bool = False,
 5099        alias_tokens: t.Collection[TokenType] | None = None,
 5100        parse_bracket: bool = False,
 5101        is_db_reference: bool = False,
 5102        parse_partition: bool = False,
 5103        consume_pipe: bool = False,
 5104    ) -> exp.Expr | None:
 5105        if not schema and not is_db_reference and not consume_pipe and not joins:
 5106            index = self._index
 5107            table = self._parse_table_parts(fast=True)
 5108
 5109            if table is not None:
 5110                curr_tt = self._curr.token_type
 5111                next_tt = self._next.token_type
 5112
 5113                fast_terminators = self.TABLE_TERMINATORS
 5114
 5115                # only return the table if we're sure there are no other operators
 5116                # MATCH_CONDITION is a special case because it accepts any alias before it like LIMIT
 5117                if curr_tt in fast_terminators and next_tt != TokenType.MATCH_CONDITION:
 5118                    return table
 5119
 5120                postfix_tokens = self.TABLE_POSTFIX_TOKENS
 5121
 5122                if curr_tt not in postfix_tokens and next_tt not in postfix_tokens:
 5123                    if alias := self._parse_table_alias(
 5124                        alias_tokens=alias_tokens or self.TABLE_ALIAS_TOKENS
 5125                    ):
 5126                        table.set("alias", alias)
 5127
 5128                    if self._curr.token_type in fast_terminators:
 5129                        return table
 5130
 5131                self._retreat(index)
 5132
 5133        if stream := self._parse_stream():
 5134            return stream
 5135
 5136        if lateral := self._parse_lateral():
 5137            return lateral
 5138
 5139        if unnest := self._parse_unnest():
 5140            return unnest
 5141
 5142        if values := self._parse_derived_table_values():
 5143            return values
 5144
 5145        if subquery := self._parse_select(table=True, consume_pipe=consume_pipe):
 5146            if not subquery.args.get("pivots"):
 5147                subquery.set("pivots", self._parse_pivots())
 5148            if joins:
 5149                for join in self._parse_joins():
 5150                    subquery.append("joins", join)
 5151            return subquery
 5152
 5153        bracket = parse_bracket and self._parse_bracket(None)
 5154        bracket = self.expression(exp.Table(this=bracket)) if bracket else None
 5155
 5156        rows_from_tables = (
 5157            self._parse_wrapped_csv(self._parse_table)
 5158            if self._match_text_seq("ROWS", "FROM")
 5159            else None
 5160        )
 5161        rows_from = (
 5162            self.expression(exp.Table(rows_from=rows_from_tables)) if rows_from_tables else None
 5163        )
 5164
 5165        only = self._match(TokenType.ONLY)
 5166
 5167        this = t.cast(
 5168            exp.Expr,
 5169            bracket
 5170            or rows_from
 5171            or self._parse_bracket(
 5172                self._parse_table_parts(schema=schema, is_db_reference=is_db_reference)
 5173            ),
 5174        )
 5175
 5176        if only:
 5177            this.set("only", only)
 5178
 5179        # Postgres supports a wildcard (table) suffix operator, which is a no-op in this context
 5180        self._match(TokenType.STAR)
 5181
 5182        parse_partition = parse_partition or self.SUPPORTS_PARTITION_SELECTION
 5183        if parse_partition and self._match(TokenType.PARTITION, advance=False):
 5184            this.set("partition", self._parse_partition())
 5185
 5186        if schema:
 5187            return self._parse_schema(this=this)
 5188
 5189        if self.dialect.ALIAS_POST_VERSION:
 5190            this.set("version", self._parse_version())
 5191
 5192        if self.dialect.ALIAS_POST_TABLESAMPLE:
 5193            this.set("sample", self._parse_table_sample())
 5194
 5195        alias = self._parse_table_alias(alias_tokens=alias_tokens or self.TABLE_ALIAS_TOKENS)
 5196        if alias:
 5197            this.set("alias", alias)
 5198
 5199            # DuckDB requires the time-travel clause to come after the alias, e.g.
 5200            # SELECT * FROM t AS a AT (VERSION => 1)
 5201            if isinstance(this, exp.Table) and not this.args.get("when"):
 5202                this.set("when", self._parse_historical_data())
 5203
 5204        if self._match(TokenType.INDEXED_BY):
 5205            this.set("indexed", self._parse_table_parts())
 5206        elif self._match_text_seq("NOT", "INDEXED"):
 5207            this.set("indexed", False)
 5208
 5209        if isinstance(this, exp.Table) and self._match_text_seq("AT"):
 5210            return self.expression(
 5211                exp.AtIndex(this=this.to_column(copy=False), expression=self._parse_id_var())
 5212            )
 5213
 5214        this.set("hints", self._parse_table_hints())
 5215
 5216        if not this.args.get("pivots"):
 5217            this.set("pivots", self._parse_pivots())
 5218
 5219        if not self.dialect.ALIAS_POST_TABLESAMPLE:
 5220            this.set("sample", self._parse_table_sample())
 5221
 5222        if not self.dialect.ALIAS_POST_VERSION:
 5223            this.set("version", self._parse_version())
 5224
 5225        if joins:
 5226            for join in self._parse_joins(alias_tokens=alias_tokens):
 5227                this.append("joins", join)
 5228
 5229        if self._match_pair(TokenType.WITH, TokenType.ORDINALITY):
 5230            this.set("ordinality", True)
 5231            this.set("alias", self._parse_table_alias())
 5232
 5233        # TABLE(<tvf>) is parsed into a Table wrapping exp.TableFromRows, so we
 5234        # hoist the table args onto the latter and return it instead
 5235        if isinstance(this, exp.Table) and isinstance(this.this, exp.TableFromRows):
 5236            table_from_rows = this.this
 5237            for arg in exp.TableFromRows.arg_types:
 5238                if arg != "this":
 5239                    table_from_rows.set(arg, this.args.get(arg))
 5240
 5241            this = table_from_rows
 5242
 5243        return this
 5244
 5245    def _parse_version(self) -> exp.Version | None:
 5246        for phrase, this in self.VERSION_PHRASES.items():
 5247            if self._match_text_seq(*phrase):
 5248                break
 5249        else:
 5250            return None
 5251
 5252        if self._match_set((TokenType.FROM, TokenType.BETWEEN)):
 5253            kind = self._prev.text.upper()
 5254            start = self._parse_bitwise()
 5255            self._match_texts(("TO", "AND"))
 5256            end = self._parse_bitwise()
 5257            expression: exp.Expr | None = self.expression(exp.Tuple(expressions=[start, end]))
 5258        elif self._match_text_seq("CONTAINED", "IN"):
 5259            kind = "CONTAINED IN"
 5260            expression = self.expression(
 5261                exp.Tuple(expressions=self._parse_wrapped_csv(self._parse_bitwise))
 5262            )
 5263        elif self._match(TokenType.ALL):
 5264            kind = "ALL"
 5265            expression = None
 5266        else:
 5267            self._match_text_seq("AS", "OF")
 5268            kind = "AS OF"
 5269            expression = self._parse_type()
 5270
 5271        return self.expression(exp.Version(this=this, expression=expression, kind=kind))
 5272
 5273    def _parse_historical_data(self) -> exp.HistoricalData | None:
 5274        # https://docs.snowflake.com/en/sql-reference/constructs/at-before
 5275        index = self._index
 5276        historical_data = None
 5277        if self._match_texts(self.HISTORICAL_DATA_PREFIX):
 5278            this = self._prev.text.upper()
 5279            kind = (
 5280                self._match(TokenType.L_PAREN)
 5281                and self._match_texts(self.HISTORICAL_DATA_KIND)
 5282                and self._prev.text.upper()
 5283            )
 5284            expression = self._match(TokenType.FARROW) and self._parse_bitwise()
 5285
 5286            if expression:
 5287                self._match_r_paren()
 5288                historical_data = self.expression(
 5289                    exp.HistoricalData(this=this, kind=kind, expression=expression)
 5290                )
 5291            else:
 5292                self._retreat(index)
 5293
 5294        return historical_data
 5295
 5296    def _parse_changes(self) -> exp.Changes | None:
 5297        if not self._match_text_seq("CHANGES", "(", "INFORMATION", "=>"):
 5298            return None
 5299
 5300        information = self._parse_var(any_token=True)
 5301        self._match_r_paren()
 5302
 5303        return self.expression(
 5304            exp.Changes(
 5305                information=information,
 5306                at_before=self._parse_historical_data(),
 5307                end=self._parse_historical_data(),
 5308            )
 5309        )
 5310
 5311    def _parse_unnest(self, with_alias: bool = True) -> exp.Unnest | None:
 5312        if not self._match_pair(TokenType.UNNEST, TokenType.L_PAREN, advance=False):
 5313            return None
 5314
 5315        self._advance()
 5316
 5317        expressions = self._parse_wrapped_csv(self._parse_equality)
 5318        offset: bool | exp.Expr = self._match_pair(TokenType.WITH, TokenType.ORDINALITY)
 5319
 5320        alias = self._parse_table_alias() if with_alias else None
 5321
 5322        if alias:
 5323            if self.dialect.UNNEST_COLUMN_ONLY:
 5324                if alias.args.get("columns"):
 5325                    self.raise_error("Unexpected extra column alias in unnest.")
 5326
 5327                alias.set("columns", [alias.this])
 5328                alias.set("this", None)
 5329
 5330            columns = alias.args.get("columns") or []
 5331            if offset and len(expressions) < len(columns):
 5332                offset = columns.pop()
 5333
 5334        if not offset and self._match_pair(TokenType.WITH, TokenType.OFFSET):
 5335            self._match(TokenType.ALIAS)
 5336            offset = self._parse_id_var(
 5337                any_token=False, tokens=self.UNNEST_OFFSET_ALIAS_TOKENS
 5338            ) or exp.to_identifier("offset")
 5339
 5340        return self.expression(exp.Unnest(expressions=expressions, alias=alias, offset=offset))
 5341
 5342    def _parse_derived_table_values(self, allow_value_synonym: bool = False) -> exp.Values | None:
 5343        is_derived = self._match_pair(TokenType.L_PAREN, TokenType.VALUES)
 5344        if not is_derived and not (
 5345            # ClickHouse's `FORMAT Values` is equivalent to `VALUES`
 5346            self._match_text_seq("VALUES")
 5347            or self._match_text_seq("FORMAT", "VALUES")
 5348            # MySQL accepts VALUE as a synonym for VALUES in INSERT statements.
 5349            or (allow_value_synonym and self._match_text_seq("VALUE"))
 5350        ):
 5351            return None
 5352
 5353        expressions = self._parse_csv(self._parse_value)
 5354        alias = self._parse_table_alias()
 5355
 5356        if is_derived:
 5357            self._match_r_paren()
 5358
 5359        return self.expression(
 5360            exp.Values(expressions=expressions, alias=alias or self._parse_table_alias())
 5361        )
 5362
 5363    def _parse_table_sample(self, as_modifier: bool = False) -> exp.TableSample | None:
 5364        if not self._match(TokenType.TABLE_SAMPLE) and not (
 5365            as_modifier and self._match_text_seq("USING", "SAMPLE")
 5366        ):
 5367            return None
 5368
 5369        bucket_numerator = None
 5370        bucket_denominator = None
 5371        bucket_field = None
 5372        percent = None
 5373        size = None
 5374        seed = None
 5375
 5376        method = self._parse_var(tokens=(TokenType.ROW,), upper=True)
 5377        matched_l_paren = self._match(TokenType.L_PAREN)
 5378
 5379        if self.TABLESAMPLE_CSV:
 5380            num = None
 5381            expressions = self._parse_csv(self._parse_primary)
 5382        else:
 5383            expressions = None
 5384            num = (
 5385                self._parse_factor(parse_mod=False)
 5386                if self._match(TokenType.NUMBER, advance=False)
 5387                else self._parse_primary() or self._parse_placeholder()
 5388            )
 5389
 5390        if self._match_text_seq("BUCKET"):
 5391            bucket_numerator = self._parse_number()
 5392            self._match_text_seq("OUT", "OF")
 5393            bucket_denominator = bucket_denominator = self._parse_number()
 5394            self._match(TokenType.ON)
 5395            bucket_field = self._parse_field()
 5396        elif self._match_set((TokenType.PERCENT, TokenType.MOD)):
 5397            percent = num
 5398        elif self._match(TokenType.ROWS) or not self.dialect.TABLESAMPLE_SIZE_IS_PERCENT:
 5399            size = num
 5400        else:
 5401            percent = num
 5402
 5403        if matched_l_paren:
 5404            self._match_r_paren()
 5405
 5406        if self._match(TokenType.L_PAREN):
 5407            method = self._parse_var(upper=True)
 5408            seed = self._match(TokenType.COMMA) and self._parse_number()
 5409            self._match_r_paren()
 5410        elif self._match_texts(("SEED", "REPEATABLE")):
 5411            seed = self._parse_wrapped(self._parse_number)
 5412
 5413        if not method and self.DEFAULT_SAMPLING_METHOD:
 5414            method = exp.var(self.DEFAULT_SAMPLING_METHOD)
 5415
 5416        return self.expression(
 5417            exp.TableSample(
 5418                expressions=expressions,
 5419                method=method,
 5420                bucket_numerator=bucket_numerator,
 5421                bucket_denominator=bucket_denominator,
 5422                bucket_field=bucket_field,
 5423                percent=percent,
 5424                size=size,
 5425                seed=seed,
 5426            )
 5427        )
 5428
 5429    def _parse_pivots(self) -> list[exp.Pivot] | None:
 5430        if self._curr.token_type not in (TokenType.PIVOT, TokenType.UNPIVOT):
 5431            return None
 5432        return list(iter(self._parse_pivot, None)) or None
 5433
 5434    def _parse_joins(
 5435        self, alias_tokens: t.Collection[TokenType] | None = None
 5436    ) -> t.Iterator[exp.Join]:
 5437        return iter(lambda: self._parse_join(alias_tokens=alias_tokens), None)
 5438
 5439    def _parse_unpivot_columns(self) -> exp.UnpivotColumns | None:
 5440        if not self._match(TokenType.INTO):
 5441            return None
 5442
 5443        return self.expression(
 5444            exp.UnpivotColumns(
 5445                this=self._match_text_seq("NAME") and self._parse_column(),
 5446                expressions=self._match_text_seq("VALUE") and self._parse_csv(self._parse_column),
 5447            )
 5448        )
 5449
 5450    # https://duckdb.org/docs/sql/statements/pivot
 5451    def _parse_simplified_pivot(self, is_unpivot: bool | None = None) -> exp.Pivot:
 5452        def _parse_on() -> exp.Expr | None:
 5453            this = self._parse_bitwise()
 5454
 5455            if self._match(TokenType.IN):
 5456                # PIVOT ... ON col IN (row_val1, row_val2)
 5457                return self._parse_in(this)
 5458            if self._match(TokenType.ALIAS, advance=False):
 5459                # UNPIVOT ... ON (col1, col2, col3) AS row_val
 5460                return self._parse_alias(this)
 5461
 5462            return this
 5463
 5464        this = self._parse_table()
 5465        expressions = self._match(TokenType.ON) and self._parse_csv(_parse_on)
 5466        into = self._parse_unpivot_columns()
 5467        using = self._match(TokenType.USING) and self._parse_csv(
 5468            lambda: self._parse_alias(self._parse_column())
 5469        )
 5470        group = self._parse_group()
 5471
 5472        return self.expression(
 5473            exp.Pivot(
 5474                this=this,
 5475                expressions=expressions,
 5476                using=using,
 5477                group=group,
 5478                unpivot=is_unpivot,
 5479                into=into,
 5480            )
 5481        )
 5482
 5483    def _parse_pivot_in(self) -> exp.In:
 5484        def _parse_aliased_expression() -> exp.Expr | None:
 5485            this = self._parse_select_or_expression()
 5486
 5487            self._match(TokenType.ALIAS)
 5488            alias = self._parse_bitwise()
 5489            if alias:
 5490                if isinstance(alias, exp.Column) and not alias.db:
 5491                    alias = alias.this
 5492                return self.expression(exp.PivotAlias(this=this, alias=alias))
 5493
 5494            return this
 5495
 5496        value = self._parse_column()
 5497
 5498        if not self._match(TokenType.IN):
 5499            self.raise_error("Expecting IN")
 5500
 5501        if self._match(TokenType.L_PAREN):
 5502            if self._match(TokenType.ANY):
 5503                exprs: list[exp.Expr] = ensure_list(exp.PivotAny(this=self._parse_order()))
 5504            else:
 5505                exprs = self._parse_csv(_parse_aliased_expression)
 5506            self._match_r_paren()
 5507            return self.expression(exp.In(this=value, expressions=exprs))
 5508
 5509        return self.expression(exp.In(this=value, field=self._parse_id_var()))
 5510
 5511    def _parse_pivot_aggregation(self) -> exp.Expr | None:
 5512        func = self._parse_function()
 5513        if not func:
 5514            if self._prev.token_type == TokenType.COMMA:
 5515                return None
 5516            self.raise_error("Expecting an aggregation function in PIVOT")
 5517
 5518        return self._parse_alias(func)
 5519
 5520    def _parse_pivot(self) -> exp.Pivot | None:
 5521        index = self._index
 5522        include_nulls = None
 5523
 5524        if self._match(TokenType.PIVOT):
 5525            unpivot = False
 5526        elif self._match(TokenType.UNPIVOT):
 5527            unpivot = True
 5528
 5529            # https://docs.databricks.com/en/sql/language-manual/sql-ref-syntax-qry-select-unpivot.html#syntax
 5530            if self._match_text_seq("INCLUDE", "NULLS"):
 5531                include_nulls = True
 5532            elif self._match_text_seq("EXCLUDE", "NULLS"):
 5533                include_nulls = False
 5534        else:
 5535            return None
 5536
 5537        expressions = []
 5538
 5539        if not self._match(TokenType.L_PAREN):
 5540            self._retreat(index)
 5541            return None
 5542
 5543        if unpivot:
 5544            expressions = self._parse_csv(self._parse_column)
 5545        else:
 5546            expressions = self._parse_csv(self._parse_pivot_aggregation)
 5547
 5548        if not expressions:
 5549            self.raise_error("Failed to parse PIVOT's aggregation list")
 5550
 5551        if not self._match(TokenType.FOR):
 5552            self.raise_error("Expecting FOR")
 5553
 5554        fields = []
 5555        while True:
 5556            field = self._try_parse(self._parse_pivot_in)
 5557            if not field:
 5558                break
 5559            fields.append(field)
 5560
 5561        default_on_null = self._match_text_seq("DEFAULT", "ON", "NULL") and self._parse_wrapped(
 5562            self._parse_bitwise
 5563        )
 5564
 5565        group = self._parse_group()
 5566
 5567        self._match_r_paren()
 5568
 5569        pivot = self.expression(
 5570            exp.Pivot(
 5571                expressions=expressions,
 5572                fields=fields,
 5573                unpivot=unpivot,
 5574                include_nulls=include_nulls,
 5575                default_on_null=default_on_null,
 5576                group=group,
 5577            )
 5578        )
 5579
 5580        if unpivot:
 5581            pivot.set("expressions", [_unpivot_target(e) for e in pivot.expressions])
 5582            for pivot_field in pivot.fields:
 5583                if isinstance(pivot_field, exp.In):
 5584                    pivot_field.set("this", _unpivot_target(pivot_field.this))
 5585
 5586            pivot.set("value_columns_first", self.UNPIVOT_VALUE_COLUMNS_FIRST)
 5587
 5588        if not self._match_set((TokenType.PIVOT, TokenType.UNPIVOT), advance=False):
 5589            pivot.set("alias", self._parse_table_alias())
 5590
 5591        if not unpivot:
 5592            names = self._pivot_column_names(t.cast(list[exp.Expr], expressions))
 5593
 5594            columns: list[exp.Expr] = []
 5595            all_fields = []
 5596            for pivot_field in pivot.fields:
 5597                pivot_field_expressions = pivot_field.expressions
 5598
 5599                # The `PivotAny` expression corresponds to `ANY ORDER BY <column>`; we can't infer in this case.
 5600                if isinstance(seq_get(pivot_field_expressions, 0), exp.PivotAny):
 5601                    continue
 5602
 5603                all_fields.append(
 5604                    [
 5605                        # An explicit `<field> AS <alias>` names the output column directly,
 5606                        # so it wins over the dialect's string-identifying convention
 5607                        fld.sql()
 5608                        if self.IDENTIFY_PIVOT_STRINGS and not isinstance(fld, exp.PivotAlias)
 5609                        else fld.alias_or_name
 5610                        for fld in pivot_field_expressions
 5611                    ]
 5612                )
 5613
 5614            if all_fields:
 5615                if names:
 5616                    all_fields.append(names)
 5617
 5618                # Generate all possible combinations of the pivot columns
 5619                # e.g PIVOT(sum(...) as total FOR year IN (2000, 2010) FOR country IN ('NL', 'US'))
 5620                # generates the product between [[2000, 2010], ['NL', 'US'], ['total']]
 5621                for fld_parts_tuple in itertools.product(*all_fields):
 5622                    fld_parts = list(fld_parts_tuple)
 5623
 5624                    if names and self.PREFIXED_PIVOT_COLUMNS:
 5625                        # Move the "name" to the front of the list
 5626                        fld_parts.insert(0, fld_parts.pop(-1))
 5627
 5628                    columns.append(exp.to_identifier("_".join(fld_parts)))
 5629
 5630            pivot.set("columns", columns)
 5631            pivot.set("identify_pivot_strings", self.IDENTIFY_PIVOT_STRINGS)
 5632            pivot.set("prefixed_pivot_columns", self.PREFIXED_PIVOT_COLUMNS)
 5633            pivot.set("pivot_column_naming", self.PIVOT_COLUMN_NAMING)
 5634
 5635        return pivot
 5636
 5637    def _pivot_column_names(self, aggregations: list[exp.Expr]) -> list[str]:
 5638        return [agg.alias for agg in aggregations if agg.alias]
 5639
 5640    def _parse_prewhere(self, skip_where_token: bool = False) -> exp.PreWhere | None:
 5641        if not skip_where_token and not self._match(TokenType.PREWHERE):
 5642            return None
 5643
 5644        comments = self._prev_comments
 5645        return self.expression(
 5646            exp.PreWhere(this=self._parse_disjunction()),
 5647            comments=comments,
 5648        )
 5649
 5650    def _parse_where(self, skip_where_token: bool = False) -> exp.Where | None:
 5651        if not skip_where_token and not self._match(TokenType.WHERE):
 5652            return None
 5653
 5654        comments = self._prev_comments
 5655        return self.expression(
 5656            exp.Where(this=self._parse_disjunction()),
 5657            comments=comments,
 5658        )
 5659
 5660    def _parse_group(self, skip_group_by_token: bool = False) -> exp.Group | None:
 5661        if not skip_group_by_token and not self._match(TokenType.GROUP_BY):
 5662            return None
 5663        comments = self._prev_comments
 5664
 5665        elements: dict[str, t.Any] = defaultdict(list)
 5666
 5667        if self._match(TokenType.ALL):
 5668            elements["all"] = True
 5669        elif self._match(TokenType.DISTINCT):
 5670            elements["all"] = False
 5671
 5672        while True:
 5673            # Stop before consuming modifier tokens like LIMIT, OFFSET and WINDOW,
 5674            # which are also valid identifiers
 5675            if self._match_set(self.QUERY_MODIFIER_TOKENS, advance=False):
 5676                break
 5677
 5678            elements["expressions"].extend(
 5679                self._parse_csv(
 5680                    lambda: (
 5681                        self._parse_grouping_sets()
 5682                        or self._parse_cube_or_rollup()
 5683                        or self._parse_disjunction()
 5684                    )
 5685                )
 5686            )
 5687
 5688            before_with_index = self._index
 5689
 5690            if self._match(TokenType.WITH) and (
 5691                cube_or_rollup := self._parse_cube_or_rollup(with_prefix=True)
 5692            ):
 5693                key = "rollup" if isinstance(cube_or_rollup, exp.Rollup) else "cube"
 5694                elements[key].append(cube_or_rollup)
 5695            elif grouping_sets := self._parse_grouping_sets():
 5696                # Hive-style suffix syntax: GROUP BY a, b GROUPING SETS (...)
 5697                elements["grouping_sets"].append(grouping_sets)
 5698                break
 5699            elif self._match_text_seq("TOTALS"):
 5700                elements["totals"] = True  # type: ignore
 5701
 5702            if before_with_index <= self._index <= before_with_index + 1:
 5703                self._retreat(before_with_index)
 5704                break
 5705
 5706        return self.expression(exp.Group(**elements), comments=comments)  # type: ignore
 5707
 5708    def _parse_cube_or_rollup(self, with_prefix: bool = False) -> exp.Cube | exp.Rollup | None:
 5709        if self._match(TokenType.CUBE):
 5710            kind: type[exp.Cube | exp.Rollup] = exp.Cube
 5711        elif self._match(TokenType.ROLLUP):
 5712            kind = exp.Rollup
 5713        else:
 5714            return None
 5715
 5716        return self.expression(
 5717            kind(expressions=[] if with_prefix else self._parse_wrapped_csv(self._parse_bitwise))
 5718        )
 5719
 5720    def _parse_grouping_sets(self) -> exp.GroupingSets | None:
 5721        if self._match(TokenType.GROUPING_SETS):
 5722            return self.expression(
 5723                exp.GroupingSets(expressions=self._parse_wrapped_csv(self._parse_grouping_set))
 5724            )
 5725        return None
 5726
 5727    def _parse_grouping_set(self) -> exp.Expr | None:
 5728        return self._parse_grouping_sets() or self._parse_cube_or_rollup() or self._parse_bitwise()
 5729
 5730    def _parse_having(self, skip_having_token: bool = False) -> exp.Having | None:
 5731        if not skip_having_token and not self._match(TokenType.HAVING):
 5732            return None
 5733        comments = self._prev_comments
 5734        return self.expression(
 5735            exp.Having(this=self._parse_disjunction()),
 5736            comments=comments,
 5737        )
 5738
 5739    def _parse_qualify(self) -> exp.Qualify | None:
 5740        if not self._match(TokenType.QUALIFY):
 5741            return None
 5742        return self.expression(exp.Qualify(this=self._parse_disjunction()))
 5743
 5744    def _parse_connect_with_prior(self) -> exp.Expr | None:
 5745        self.NO_PAREN_FUNCTION_PARSERS["PRIOR"] = lambda self: self.expression(
 5746            exp.Prior(this=self._parse_bitwise())
 5747        )
 5748        connect = self._parse_disjunction()
 5749        self.NO_PAREN_FUNCTION_PARSERS.pop("PRIOR")
 5750        return connect
 5751
 5752    def _parse_connect(self, skip_start_token: bool = False) -> exp.Connect | None:
 5753        if skip_start_token:
 5754            start = None
 5755        elif self._match_text_seq("START", "WITH"):
 5756            start = self._parse_disjunction()
 5757        else:
 5758            return None
 5759
 5760        self._match(TokenType.CONNECT_BY)
 5761        nocycle = self._match_text_seq("NOCYCLE")
 5762        connect = self._parse_connect_with_prior()
 5763
 5764        if not start and self._match_text_seq("START", "WITH"):
 5765            start = self._parse_disjunction()
 5766
 5767        return self.expression(exp.Connect(start=start, connect=connect, nocycle=nocycle))
 5768
 5769    def _parse_name_as_expression(self) -> exp.Expr | None:
 5770        this = self._parse_id_var(any_token=True)
 5771        if self._match(TokenType.ALIAS):
 5772            this = self.expression(exp.Alias(alias=this, this=self._parse_disjunction()))
 5773        return this
 5774
 5775    def _parse_interpolate(self) -> list[exp.Expr] | None:
 5776        if self._match_text_seq("INTERPOLATE"):
 5777            return self._parse_wrapped_csv(self._parse_name_as_expression)
 5778        return None
 5779
 5780    def _parse_order(
 5781        self, this: exp.Expr | None = None, skip_order_token: bool = False
 5782    ) -> exp.Expr | None:
 5783        siblings = None
 5784        if not skip_order_token and not self._match(TokenType.ORDER_BY):
 5785            if not self._match(TokenType.ORDER_SIBLINGS_BY):
 5786                return this
 5787
 5788            siblings = True
 5789
 5790        comments = self._prev_comments
 5791        return self.expression(
 5792            exp.Order(
 5793                this=this,
 5794                expressions=self._parse_csv(self._parse_ordered),
 5795                siblings=siblings,
 5796            ),
 5797            comments=comments,
 5798        )
 5799
 5800    def _parse_sort(self, exp_class: type[E], token: TokenType) -> E | None:
 5801        if not self._match(token):
 5802            return None
 5803        return self.expression(exp_class(expressions=self._parse_csv(self._parse_ordered)))
 5804
 5805    def _parse_ordered(
 5806        self, parse_method: t.Callable[[], exp.Expr | None] | None = None
 5807    ) -> exp.Ordered | None:
 5808        this = parse_method() if parse_method else self._parse_disjunction()
 5809        if not this:
 5810            return None
 5811
 5812        if this.name.upper() == "ALL" and self.dialect.SUPPORTS_ORDER_BY_ALL:
 5813            this = exp.var("ALL")
 5814
 5815        asc = self._match(TokenType.ASC)
 5816        desc: bool | None = True if self._match(TokenType.DESC) else (False if asc else None)
 5817
 5818        is_nulls_first = self._match_text_seq("NULLS", "FIRST")
 5819        is_nulls_last = self._match_text_seq("NULLS", "LAST")
 5820
 5821        nulls_first = is_nulls_first or False
 5822        explicitly_null_ordered = is_nulls_first or is_nulls_last
 5823
 5824        if (
 5825            not explicitly_null_ordered
 5826            and (
 5827                (not desc and self.dialect.NULL_ORDERING == "nulls_are_small")
 5828                or (desc and self.dialect.NULL_ORDERING != "nulls_are_small")
 5829            )
 5830            and self.dialect.NULL_ORDERING != "nulls_are_last"
 5831        ):
 5832            nulls_first = True
 5833
 5834        if self._match_text_seq("WITH", "FILL"):
 5835            with_fill = self.expression(
 5836                exp.WithFill(
 5837                    from_=self._match(TokenType.FROM) and self._parse_bitwise(),
 5838                    to=self._match_text_seq("TO") and self._parse_bitwise(),
 5839                    step=self._match_text_seq("STEP") and self._parse_bitwise(),
 5840                    interpolate=self._parse_interpolate(),
 5841                )
 5842            )
 5843        else:
 5844            with_fill = None
 5845
 5846        return self.expression(
 5847            exp.Ordered(this=this, desc=desc, nulls_first=nulls_first, with_fill=with_fill)
 5848        )
 5849
 5850    def _parse_limit_options(self) -> exp.LimitOptions | None:
 5851        percent = self._match_set((TokenType.PERCENT, TokenType.MOD))
 5852        rows = self._match_texts(("ROW", "ROWS"))
 5853        self._match_text_seq("ONLY")
 5854        with_ties = self._match_text_seq("WITH", "TIES")
 5855
 5856        if not (percent or rows or with_ties):
 5857            return None
 5858
 5859        return self.expression(exp.LimitOptions(percent=percent, rows=rows, with_ties=with_ties))
 5860
 5861    def _parse_limit(
 5862        self,
 5863        this: exp.Expr | None = None,
 5864        top: bool = False,
 5865        skip_limit_token: bool = False,
 5866    ) -> exp.Expr | None:
 5867        if skip_limit_token or self._match(TokenType.TOP if top else TokenType.LIMIT):
 5868            comments = self._prev_comments
 5869            if top:
 5870                limit_paren = self._match(TokenType.L_PAREN)
 5871                expression = (
 5872                    self._parse_term() or self._parse_select()
 5873                    if limit_paren
 5874                    else self._parse_number()
 5875                )
 5876
 5877                if limit_paren:
 5878                    self._match_r_paren()
 5879
 5880            else:
 5881                if self.dialect.SUPPORTS_LIMIT_ALL and self._match(TokenType.ALL):
 5882                    expression = exp.var("ALL")
 5883                else:
 5884                    expression = self._parse_term(parse_mod=False)
 5885            limit_options = self._parse_limit_options()
 5886
 5887            if self._match(TokenType.COMMA):
 5888                offset = expression
 5889                expression = self._parse_term()
 5890            else:
 5891                offset = None
 5892
 5893            limit_exp = self.expression(
 5894                exp.Limit(
 5895                    this=this,
 5896                    expression=expression,
 5897                    offset=offset,
 5898                    limit_options=limit_options,
 5899                    expressions=self._parse_limit_by(),
 5900                ),
 5901                comments=comments,
 5902            )
 5903
 5904            if top:
 5905                limit_exp.meta["top"] = True
 5906
 5907            return limit_exp
 5908
 5909        if self._match(TokenType.FETCH):
 5910            direction = (
 5911                self._prev.text.upper()
 5912                if self._match_set((TokenType.FIRST, TokenType.NEXT))
 5913                else "FIRST"
 5914            )
 5915
 5916            count = (
 5917                None
 5918                if self._match_texts(("ROW", "ROWS"), advance=False)
 5919                else self._parse_field(tokens=self.FETCH_TOKENS)
 5920            )
 5921
 5922            return self.expression(
 5923                exp.Fetch(
 5924                    direction=direction, count=count, limit_options=self._parse_limit_options()
 5925                )
 5926            )
 5927
 5928        return this
 5929
 5930    def _parse_offset(self, this: exp.Expr | None = None) -> exp.Expr | None:
 5931        if not self._match(TokenType.OFFSET):
 5932            return this
 5933
 5934        count = self._parse_term()
 5935        self._match_texts(("ROW", "ROWS"))
 5936
 5937        return self.expression(
 5938            exp.Offset(this=this, expression=count, expressions=self._parse_limit_by())
 5939        )
 5940
 5941    def _can_parse_limit_or_offset(self) -> bool:
 5942        if not self._match_set(self.AMBIGUOUS_ALIAS_TOKENS, advance=False):
 5943            return False
 5944
 5945        index = self._index
 5946        result = bool(
 5947            self._try_parse(self._parse_limit, retreat=True)
 5948            or self._try_parse(self._parse_offset, retreat=True)
 5949        )
 5950        self._retreat(index)
 5951
 5952        # MATCH_CONDITION (...) is a special construct that should not be consumed by limit/offset
 5953        if self._next.token_type == TokenType.MATCH_CONDITION:
 5954            result = False
 5955
 5956        return result
 5957
 5958    def _can_parse_named_window(self) -> bool:
 5959        # `WINDOW` is in ID_VAR_TOKENS so it could be mistakenly consumed as an implicit alias.
 5960        # Refuse only when the following tokens look like a named-window clause: `WINDOW <id> AS (`.
 5961        if not self._match(TokenType.WINDOW, advance=False):
 5962            return False
 5963
 5964        name = self._tokens[self._index + 1] if self._index + 1 < len(self._tokens) else None
 5965        if name is None or name.token_type not in self.ID_VAR_TOKENS:
 5966            return False
 5967
 5968        alias_tok = self._tokens[self._index + 2] if self._index + 2 < len(self._tokens) else None
 5969        if alias_tok is None or alias_tok.token_type != TokenType.ALIAS:
 5970            return False
 5971
 5972        body = self._tokens[self._index + 3] if self._index + 3 < len(self._tokens) else None
 5973        return body is not None and body.token_type == TokenType.L_PAREN
 5974
 5975    def _parse_limit_by(self) -> list[exp.Expr] | None:
 5976        return self._parse_csv(self._parse_bitwise) if self._match_text_seq("BY") else None
 5977
 5978    def _parse_locks(self) -> list[exp.Lock]:
 5979        locks = []
 5980        while True:
 5981            update, key = None, None
 5982            if self._match_text_seq("FOR", "UPDATE"):
 5983                update = True
 5984            elif self._match_text_seq("FOR", "SHARE") or self._match_text_seq(
 5985                "LOCK", "IN", "SHARE", "MODE"
 5986            ):
 5987                update = False
 5988            elif self._match_text_seq("FOR", "KEY", "SHARE"):
 5989                update, key = False, True
 5990            elif self._match_text_seq("FOR", "NO", "KEY", "UPDATE"):
 5991                update, key = True, True
 5992            else:
 5993                break
 5994
 5995            expressions = None
 5996            if self._match_text_seq("OF"):
 5997                expressions = self._parse_csv(lambda: self._parse_table(schema=True))
 5998
 5999            wait: bool | exp.Expr | None = None
 6000            if self._match_text_seq("NOWAIT"):
 6001                wait = True
 6002            elif self._match_text_seq("WAIT"):
 6003                wait = self._parse_primary()
 6004            elif self._match_text_seq("SKIP", "LOCKED"):
 6005                wait = False
 6006
 6007            locks.append(
 6008                self.expression(
 6009                    exp.Lock(update=update, expressions=expressions, wait=wait, key=key)
 6010                )
 6011            )
 6012
 6013        return locks
 6014
 6015    def parse_set_operation(
 6016        self, this: exp.Expr | None, consume_pipe: bool = False
 6017    ) -> exp.Expr | None:
 6018        start = self._index
 6019        _, side_token, kind_token = self._parse_join_parts()
 6020
 6021        side = side_token.text if side_token else None
 6022        kind = kind_token.text if kind_token else None
 6023
 6024        if not self._match_set(self.SET_OPERATIONS):
 6025            self._retreat(start)
 6026            return None
 6027
 6028        token_type = self._prev.token_type
 6029
 6030        if token_type == TokenType.UNION:
 6031            operation: type[exp.SetOperation] = exp.Union
 6032        elif token_type == TokenType.EXCEPT:
 6033            operation = exp.Except
 6034        else:
 6035            operation = exp.Intersect
 6036
 6037        comments = self._prev.comments
 6038
 6039        if self._match(TokenType.DISTINCT):
 6040            distinct: bool | None = True
 6041        elif self._match(TokenType.ALL):
 6042            distinct = False
 6043        else:
 6044            distinct = self.dialect.SET_OP_DISTINCT_BY_DEFAULT[operation]
 6045            if distinct is None:
 6046                self.raise_error(f"Expected DISTINCT or ALL for {operation.__name__}")
 6047
 6048        by_name = (
 6049            self._match_text_seq("BY", "NAME")
 6050            or self._match_text_seq("STRICT", "CORRESPONDING")
 6051            or None
 6052        )
 6053        if self._match_text_seq("CORRESPONDING"):
 6054            by_name = True
 6055            if not side and not kind:
 6056                kind = "INNER"
 6057
 6058        on_column_list = None
 6059        if by_name and self._match_texts(("ON", "BY")):
 6060            on_column_list = self._parse_wrapped_csv(self._parse_column)
 6061
 6062        expression = self._parse_select(
 6063            nested=True, parse_set_operation=False, consume_pipe=consume_pipe
 6064        )
 6065
 6066        # Wrap VALUES operands in selects, both for consistency with the CTE canonicalization
 6067        # in _parse_cte and so that alias pushdown can reach into set operation branches
 6068        if isinstance(this, exp.Values):
 6069            this = self._values_to_select(this)
 6070        if isinstance(expression, exp.Values):
 6071            expression = self._values_to_select(expression)
 6072
 6073        if isinstance(this, exp.Alias) and isinstance(this.this, exp.Subquery):
 6074            subquery = this.this
 6075            subquery.set("alias", exp.TableAlias(this=this.args["alias"]))
 6076            subquery.add_comments(this.pop_comments())
 6077            this = subquery
 6078
 6079        return self.expression(
 6080            operation(
 6081                this=this,
 6082                distinct=distinct,
 6083                by_name=by_name,
 6084                expression=expression,
 6085                side=side,
 6086                kind=kind,
 6087                on=on_column_list,
 6088            ),
 6089            comments=comments,
 6090        )
 6091
 6092    def _parse_set_operations(self, this: exp.Expr | None) -> exp.Expr | None:
 6093        while this:
 6094            setop = self.parse_set_operation(this)
 6095            if not setop:
 6096                break
 6097            this = setop
 6098
 6099        if isinstance(this, exp.SetOperation) and self.MODIFIERS_ATTACHED_TO_SET_OP:
 6100            expression = this.expression
 6101
 6102            if expression:
 6103                for arg in self.SET_OP_MODIFIERS:
 6104                    expr = expression.args.get(arg)
 6105                    if expr and not (arg == "limit" and expr.meta.get("top")):
 6106                        expression.set(arg, None)
 6107                        this.set(arg, expr)
 6108
 6109            # A trailing LIMIT/FETCH can coexist with TOP on the final operand.
 6110            if self._curr.token_type in (TokenType.LIMIT, TokenType.FETCH):
 6111                this = self._parse_query_modifiers(this)
 6112
 6113        return this
 6114
 6115    def _parse_expression(self) -> exp.Expr | None:
 6116        return self._parse_alias(self._parse_assignment())
 6117
 6118    def _parse_assignment(self) -> exp.Expr | None:
 6119        this = self._parse_disjunction()
 6120        if not this and self._next.token_type in self.ASSIGNMENT:
 6121            # This allows us to parse <non-identifier token> := <expr>
 6122            this = exp.column(
 6123                t.cast(str, self._advance_any(ignore_reserved=True) and self._prev.text)
 6124            )
 6125
 6126        while self._match_set(self.ASSIGNMENT):
 6127            if isinstance(this, exp.Column) and len(this.parts) == 1:
 6128                this = this.this
 6129
 6130            comments = self._prev_comments
 6131            this = self.expression(
 6132                self.ASSIGNMENT[self._prev.token_type](
 6133                    this=this, expression=self._parse_assignment()
 6134                ),
 6135                comments=comments,
 6136            )
 6137
 6138        return this
 6139
 6140    def _parse_disjunction(self) -> exp.Expr | None:
 6141        this = self._parse_conjunction()
 6142        while self._match_set(self.DISJUNCTION):
 6143            comments = self._prev_comments
 6144            this = self.expression(
 6145                self.DISJUNCTION[self._prev.token_type](
 6146                    this=this, expression=self._parse_conjunction()
 6147                ),
 6148                comments=comments,
 6149            )
 6150        return this
 6151
 6152    def _parse_conjunction(self) -> exp.Expr | None:
 6153        this = self._parse_equality()
 6154        while self._match_set(self.CONJUNCTION):
 6155            comments = self._prev_comments
 6156            this = self.expression(
 6157                self.CONJUNCTION[self._prev.token_type](
 6158                    this=this, expression=self._parse_equality()
 6159                ),
 6160                comments=comments,
 6161            )
 6162        return this
 6163
 6164    def _parse_equality(self) -> exp.Expr | None:
 6165        this = self._parse_comparison()
 6166        while self._match_set(self.EQUALITY):
 6167            comments = self._prev_comments
 6168            this = self.expression(
 6169                self.EQUALITY[self._prev.token_type](
 6170                    this=this, expression=self._parse_comparison()
 6171                ),
 6172                comments=comments,
 6173            )
 6174        return this
 6175
 6176    def _parse_comparison(self) -> exp.Expr | None:
 6177        this = self._parse_range()
 6178        while self._match_set(self.COMPARISON):
 6179            comments = self._prev_comments
 6180            this = self.expression(
 6181                self.COMPARISON[self._prev.token_type](this=this, expression=self._parse_range()),
 6182                comments=comments,
 6183            )
 6184        return this
 6185
 6186    def _parse_range(self, this: exp.Expr | None = None) -> exp.Expr | None:
 6187        this = this or self._parse_bitwise()
 6188
 6189        while True:
 6190            negate = self._match(TokenType.NOT)
 6191            if self._match_set(self.RANGE_PARSERS):
 6192                expression = self.RANGE_PARSERS[self._prev.token_type](self, this)
 6193                if not expression:
 6194                    return this
 6195
 6196                this = expression
 6197            elif self._match(TokenType.ISNULL) or (negate and self._match(TokenType.NULL)):
 6198                this = self.expression(exp.Is(this=this, expression=exp.Null()))
 6199            elif self._match(TokenType.NOTNULL):
 6200                # Postgres supports ISNULL and NOTNULL for conditions.
 6201                # https://blog.andreiavram.ro/postgresql-null-composite-type/
 6202                if self.dialect.NORMALIZE_NOT_NULL:
 6203                    this = self.expression(exp.Is(this=this, expression=exp.Null()))
 6204                    this = self.expression(exp.Not(this=this))
 6205                else:
 6206                    this = self.expression(exp.Is(this=this, expression=exp.Null(), negate=True))
 6207            else:
 6208                if negate:
 6209                    self._retreat(self._index - 1)
 6210                break
 6211
 6212            if negate:
 6213                this = self._negate_range(this)
 6214                if self._curr and (
 6215                    self._curr.token_type == TokenType.NOT
 6216                    or self._curr.token_type in self.RANGE_PARSERS
 6217                ):
 6218                    this = self.expression(exp.Paren(this=this))
 6219
 6220        return this
 6221
 6222    def _negate_range(self, this: exp.Expr | None = None) -> exp.Expr | None:
 6223        if not this:
 6224            return this
 6225
 6226        expression = this.this if isinstance(this, exp.Escape) else this
 6227        if isinstance(expression, (exp.Like, exp.ILike)):
 6228            expression.set("negate", True)
 6229            return this
 6230
 6231        return self.expression(exp.Not(this=this))
 6232
 6233    def _parse_is(self, this: exp.Expr | None) -> exp.Expr | None:
 6234        index = self._index - 1
 6235        negate = self._match(TokenType.NOT)
 6236
 6237        if self._match_text_seq("DISTINCT", "FROM"):
 6238            klass = exp.NullSafeEQ if negate else exp.NullSafeNEQ
 6239            return self.expression(klass(this=this, expression=self._parse_bitwise()))
 6240
 6241        if self._match(TokenType.JSON):
 6242            kind = self._match_texts(self.IS_JSON_PREDICATE_KIND) and self._prev.text.upper()
 6243
 6244            if self._match_text_seq("WITH"):
 6245                _with = True
 6246            elif self._match_text_seq("WITHOUT"):
 6247                _with = False
 6248            else:
 6249                _with = None
 6250
 6251            unique = self._match(TokenType.UNIQUE)
 6252            self._match_text_seq("KEYS")
 6253            expression: exp.Expr | None = self.expression(
 6254                exp.JSON(this=kind, with_=_with, unique=unique)
 6255            )
 6256        else:
 6257            expression = self._parse_null() or self._parse_bitwise()
 6258            if not expression:
 6259                self._retreat(index)
 6260                return None
 6261
 6262        if negate and isinstance(expression, exp.Null) and not self.dialect.NORMALIZE_NOT_NULL:
 6263            this = self.expression(exp.Is(this=this, expression=expression, negate=True))
 6264        else:
 6265            this = self.expression(exp.Is(this=this, expression=expression))
 6266            this = self.expression(exp.Not(this=this)) if negate else this
 6267
 6268        return self._parse_column_ops(this)
 6269
 6270    def _parse_in(self, this: exp.Expr | None, alias: bool = False) -> exp.In:
 6271        unnest = self._parse_unnest(with_alias=False)
 6272        if unnest:
 6273            this = self.expression(exp.In(this=this, unnest=unnest))
 6274        elif self._match_set((TokenType.L_PAREN, TokenType.L_BRACKET)):
 6275            matched_l_paren = self._prev.token_type == TokenType.L_PAREN
 6276            expressions = self._parse_csv(lambda: self._parse_select_or_expression(alias=alias))
 6277
 6278            if len(expressions) == 1 and isinstance(query := expressions[0], exp.Query):
 6279                this = self.expression(
 6280                    exp.In(this=this, query=self._parse_query_modifiers(query).subquery(copy=False))
 6281                )
 6282            else:
 6283                this = self.expression(exp.In(this=this, expressions=expressions))
 6284
 6285            if matched_l_paren:
 6286                self._match_r_paren(this)
 6287            elif not self._match(TokenType.R_BRACKET, expression=this):
 6288                self.raise_error("Expecting ]")
 6289        else:
 6290            this = self.expression(exp.In(this=this, field=self._parse_column()))
 6291
 6292        return this
 6293
 6294    def _parse_between(self, this: exp.Expr | None) -> exp.Between:
 6295        symmetric = None
 6296        if self._match_text_seq("SYMMETRIC"):
 6297            symmetric = True
 6298        elif self._match_text_seq("ASYMMETRIC"):
 6299            symmetric = False
 6300
 6301        low = self._parse_bitwise()
 6302        self._match(TokenType.AND)
 6303        high = self._parse_bitwise()
 6304
 6305        return self.expression(exp.Between(this=this, low=low, high=high, symmetric=symmetric))
 6306
 6307    def _parse_escape(self, this: exp.Expr | None) -> exp.Expr | None:
 6308        if not self._match(TokenType.ESCAPE):
 6309            return this
 6310        return self.expression(
 6311            exp.Escape(this=this, expression=self._parse_string() or self._parse_null())
 6312        )
 6313
 6314    def _parse_interval_span(
 6315        self, this: exp.Expr, parse_function_unit: bool = True
 6316    ) -> exp.Interval:
 6317        # handle day-time format interval span with omitted units:
 6318        #   INTERVAL '<number days> hh[:][mm[:ss[.ff]]]' <maybe `unit TO unit`>
 6319        interval_span_units_omitted = None
 6320        if (
 6321            this
 6322            and this.is_string
 6323            and self.SUPPORTS_OMITTED_INTERVAL_SPAN_UNIT
 6324            and exp.INTERVAL_DAY_TIME_RE.match(this.name)
 6325        ):
 6326            index = self._index
 6327
 6328            # Var "TO" Var
 6329            first_unit = self._parse_var(any_token=True, upper=True)
 6330            second_unit = None
 6331            if first_unit and self._match_text_seq("TO"):
 6332                second_unit = self._parse_var(any_token=True, upper=True)
 6333
 6334            interval_span_units_omitted = not (first_unit and second_unit)
 6335
 6336            self._retreat(index)
 6337
 6338        unit_index = self._index
 6339        if interval_span_units_omitted:
 6340            unit = None
 6341        else:
 6342            # Only attempt to parse a unit if the current token can actually be one, so that a
 6343            # trailing operator isn't swallowed, e.g. INTERVAL '1 day' AND (x)
 6344            is_unit = self._curr is not None and (
 6345                self._curr.token_type == TokenType.VAR
 6346                or self._curr.text.upper() in self.dialect.VALID_INTERVAL_UNITS
 6347            )
 6348            unit = self._parse_function() if parse_function_unit and is_unit else None
 6349            if not unit and is_unit:
 6350                unit = self._parse_var(any_token=True, upper=True)
 6351
 6352        # Most dialects support, e.g., the form INTERVAL '5' day, thus we try to parse
 6353        # each INTERVAL expression into this canonical form so it's easy to transpile
 6354        if this and this.is_number:
 6355            try:
 6356                this = exp.Literal.string(this.to_py())
 6357            except ValueError:
 6358                self.raise_error(f"Invalid numeric interval literal: {this.name!r}")
 6359        elif this and this.is_string:
 6360            parts = exp.INTERVAL_STRING_RE.findall(this.name)
 6361            if parts and unit:
 6362                # Unconsume the eagerly-parsed unit, since the real unit was part of the string
 6363                unit = None
 6364                self._retreat(unit_index)
 6365
 6366            if len(parts) == 1:
 6367                this = exp.Literal.string(parts[0][0])
 6368                unit = self.expression(exp.Var(this=parts[0][1].upper()))
 6369
 6370        if self.INTERVAL_SPANS and self._match_text_seq("TO"):
 6371            unit = self.expression(
 6372                exp.IntervalSpan(
 6373                    this=unit,
 6374                    expression=self._parse_function()
 6375                    or self._parse_var(any_token=True, upper=True),
 6376                )
 6377            )
 6378
 6379        return self.expression(exp.Interval(this=this, unit=unit))
 6380
 6381    def _parse_interval(
 6382        self, require_interval: bool = True, parse_function_unit: bool = True
 6383    ) -> exp.Add | exp.Interval | None:
 6384        index = self._index
 6385
 6386        if not self._match(TokenType.INTERVAL) and require_interval:
 6387            return None
 6388
 6389        if self._match(TokenType.STRING, advance=False):
 6390            this = self._parse_primary()
 6391        else:
 6392            this = self._parse_term()
 6393
 6394        if not this or (
 6395            isinstance(this, exp.Column)
 6396            and not this.table
 6397            and not this.this.quoted
 6398            and self._curr
 6399            and self._curr.text.upper() not in self.dialect.VALID_INTERVAL_UNITS
 6400        ):
 6401            self._retreat(index)
 6402            return None
 6403
 6404        interval = self._parse_interval_span(this, parse_function_unit=parse_function_unit)
 6405
 6406        index = self._index
 6407        self._match(TokenType.PLUS)
 6408
 6409        # Convert INTERVAL 'val_1' unit_1 [+] ... [+] 'val_n' unit_n into a sum of intervals
 6410        if self._match_set((TokenType.STRING, TokenType.NUMBER), advance=False):
 6411            return self.expression(
 6412                exp.Add(
 6413                    this=interval,
 6414                    expression=self._parse_interval(False, parse_function_unit=parse_function_unit),
 6415                )
 6416            )
 6417
 6418        self._retreat(index)
 6419        return interval
 6420
 6421    def _parse_bitwise(self) -> exp.Expr | None:
 6422        this = self._parse_term()
 6423
 6424        while True:
 6425            if self._match_set(self.BITWISE):
 6426                this = self.expression(
 6427                    self.BITWISE[self._prev.token_type](this=this, expression=self._parse_term())
 6428                )
 6429            elif self.dialect.DPIPE_IS_STRING_CONCAT and self._match(TokenType.DPIPE):
 6430                this = self.expression(
 6431                    exp.DPipe(
 6432                        this=this,
 6433                        expression=self._parse_term(),
 6434                        safe=not self.dialect.STRICT_STRING_CONCAT,
 6435                    )
 6436                )
 6437            elif self._match(TokenType.DQMARK):
 6438                this = self.expression(
 6439                    exp.Coalesce(this=this, expressions=ensure_list(self._parse_term()))
 6440                )
 6441            elif self._match_pair(TokenType.LT, TokenType.LT):
 6442                this = self.expression(
 6443                    exp.BitwiseLeftShift(this=this, expression=self._parse_term())
 6444                )
 6445            elif self._match_pair(TokenType.GT, TokenType.GT):
 6446                this = self.expression(
 6447                    exp.BitwiseRightShift(this=this, expression=self._parse_term())
 6448                )
 6449            elif self.JSON_OPERATORS and self._match_set(self.JSON_OPERATORS):
 6450                this = self.JSON_OPERATORS[self._prev.token_type](self, this, self._parse_term())
 6451            else:
 6452                break
 6453
 6454        return this
 6455
 6456    def _parse_term(self, parse_mod: bool = True) -> exp.Expr | None:
 6457        this = self._parse_factor(parse_mod=parse_mod)
 6458
 6459        while self._match_set(self.TERM):
 6460            klass = self.TERM[self._prev.token_type]
 6461            comments = self._prev_comments
 6462            expression = self._parse_factor(parse_mod=parse_mod)
 6463
 6464            this = self.expression(klass(this=this, expression=expression), comments=comments)
 6465
 6466            if isinstance(this, exp.Collate):
 6467                self._normalize_collate(this)
 6468
 6469        return this
 6470
 6471    def _normalize_collate(self, collate: exp.Collate) -> None:
 6472        expr = collate.expression
 6473
 6474        # Preserve collations such as pg_catalog."default" (Postgres) as columns, otherwise
 6475        # fallback to Identifier / Var
 6476        if isinstance(expr, exp.Column) and len(expr.parts) == 1:
 6477            ident = expr.this
 6478            if isinstance(ident, exp.Identifier):
 6479                collate.set("expression", ident if ident.quoted else exp.var(ident.name))
 6480
 6481    def _parse_factor(self, parse_mod: bool = True) -> exp.Expr | None:
 6482        parse_method = self._parse_factor_operand
 6483        this = self._parse_at_time_zone(parse_method())
 6484
 6485        while self._match_set(self.FACTOR, advance=False):
 6486            if not parse_mod and self._curr.token_type == TokenType.MOD:
 6487                break
 6488
 6489            self._advance()
 6490            klass = self.FACTOR[self._prev.token_type]
 6491            comments = self._prev_comments
 6492            expression = parse_method()
 6493
 6494            if not expression and klass is exp.IntDiv and self._prev.text.isalpha():
 6495                self._retreat(self._index - 1)
 6496                return this
 6497
 6498            this = self.expression(klass(this=this, expression=expression), comments=comments)
 6499
 6500            if isinstance(this, exp.Div):
 6501                this.set("typed", self.dialect.TYPED_DIVISION)
 6502                this.set("safe", self.dialect.SAFE_DIVISION)
 6503
 6504        return this
 6505
 6506    def _parse_factor_operand(self) -> exp.Expr | None:
 6507        return self._parse_exponent() if self.EXPONENT else self._parse_unary()
 6508
 6509    def _parse_exponent(self) -> exp.Expr | None:
 6510        this = self._parse_unary()
 6511        while self._match_set(self.EXPONENT):
 6512            comments = self._prev_comments
 6513            this = self.expression(
 6514                self.EXPONENT[self._prev.token_type](this=this, expression=self._parse_unary()),
 6515                comments=comments,
 6516            )
 6517        return this
 6518
 6519    def _parse_unary(self) -> exp.Expr | None:
 6520        if self._match_set(self.UNARY_PARSERS):
 6521            return self.UNARY_PARSERS[self._prev.token_type](self)
 6522        return self._parse_type()
 6523
 6524    def _parse_type(
 6525        self, parse_interval: bool = True, fallback_to_identifier: bool = False
 6526    ) -> exp.Expr | None:
 6527        if not fallback_to_identifier and (atom := self._parse_atom()) is not None:
 6528            return atom
 6529
 6530        if interval := parse_interval and self._parse_interval():
 6531            return self._parse_column_ops(interval)
 6532
 6533        index = self._index
 6534        data_type = self._parse_types(check_func=True, allow_identifiers=False)
 6535
 6536        # parse_types() returns a Cast if we parsed BQ's inline constructor <type>(<values>) e.g.
 6537        # STRUCT<a INT, b STRING>(1, 'foo'), which is canonicalized to CAST(<values> AS <type>)
 6538        if isinstance(data_type, exp.Cast):
 6539            # This constructor can contain ops directly after it, for instance struct unnesting:
 6540            # STRUCT<a INT, b STRING>(1, 'foo').* --> CAST(STRUCT(1, 'foo') AS STRUCT<a iNT, b STRING).*
 6541            return self._parse_column_ops(data_type)
 6542
 6543        if data_type:
 6544            index2 = self._index
 6545            this = self._parse_primary()
 6546
 6547            if isinstance(this, exp.Literal):
 6548                literal = this.name
 6549                this = self._parse_column_ops(this)
 6550
 6551                if self.ZONE_AWARE_TIMESTAMP_CONSTRUCTOR and TIME_ZONE_RE.search(literal):
 6552                    if data_type.is_type(exp.DType.TIMESTAMP):
 6553                        data_type = exp.DType.TIMESTAMPTZ.into_expr()
 6554                    elif data_type.is_type(exp.DType.TIME):
 6555                        data_type = exp.DType.TIMETZ.into_expr()
 6556
 6557                parser = self.TYPE_LITERAL_PARSERS.get(data_type.this)
 6558                if parser:
 6559                    return parser(self, this, data_type)
 6560
 6561                return self.expression(exp.Cast(this=this, to=data_type))
 6562
 6563            # The expressions arg gets set by the parser when we have something like DECIMAL(38, 0)
 6564            # in the input SQL. In that case, we'll produce these tokens: DECIMAL ( 38 , 0 )
 6565            #
 6566            # If the index difference here is greater than 1, that means the parser itself must have
 6567            # consumed additional tokens such as the DECIMAL scale and precision in the above example.
 6568            #
 6569            # If it's not greater than 1, then it must be 1, because we've consumed at least the type
 6570            # keyword, meaning that the expressions arg of the DataType must have gotten set by a
 6571            # callable in the TYPE_CONVERTERS mapping. For example, Snowflake converts DECIMAL to
 6572            # DECIMAL(38, 0)) in order to facilitate the data type's transpilation.
 6573            #
 6574            # In these cases, we don't really want to return the converted type, but instead retreat
 6575            # and try to parse a Column or Identifier in the section below.
 6576            if data_type.expressions and index2 - index > 1:
 6577                self._retreat(index2)
 6578                return self._parse_column_ops(data_type)
 6579
 6580            self._retreat(index)
 6581
 6582        if fallback_to_identifier:
 6583            return self._parse_id_var()
 6584
 6585        return self._parse_column()
 6586
 6587    def _parse_type_size(self) -> exp.DataTypeParam | None:
 6588        this = self._parse_type()
 6589        if not this:
 6590            return None
 6591
 6592        if isinstance(this, exp.Column) and not this.table:
 6593            this = exp.var(this.name.upper())
 6594
 6595        return self.expression(
 6596            exp.DataTypeParam(this=this, expression=self._parse_var(any_token=True))
 6597        )
 6598
 6599    def _parse_user_defined_type(self, identifier: exp.Identifier) -> exp.Expr | None:
 6600        type_name = identifier.name
 6601
 6602        while self._match(TokenType.DOT):
 6603            type_name = f"{type_name}.{self._advance_any() and self._prev.text}"
 6604
 6605        return exp.DataType.from_str(type_name, dialect=self.dialect, udt=True)
 6606
 6607    def _parse_types(
 6608        self,
 6609        check_func: bool = False,
 6610        schema: bool = False,
 6611        allow_identifiers: bool = True,
 6612        with_collation: bool = False,
 6613    ) -> exp.Expr | None:
 6614        index = self._index
 6615        this: exp.Expr | None = None
 6616
 6617        if self._match_set(self.TYPE_TOKENS):
 6618            type_token = self._prev.token_type
 6619        else:
 6620            type_token = None
 6621            identifier = allow_identifiers and self._parse_id_var(
 6622                any_token=False, tokens=(TokenType.VAR,)
 6623            )
 6624            if isinstance(identifier, exp.Identifier):
 6625                if identifier.quoted and identifier.name in self.QUOTED_TYPES_TO_PRESERVE:
 6626                    this = exp.DataType.build(identifier, udt=True)
 6627                else:
 6628                    try:
 6629                        tokens = self.dialect.tokenize(identifier.name)
 6630                    except TokenError:
 6631                        tokens = None
 6632
 6633                    if tokens and (type_token := tokens[0].token_type) in self.TYPE_TOKENS:
 6634                        if len(tokens) > 1:
 6635                            return exp.DataType.from_str(identifier.name, dialect=self.dialect)
 6636                    elif self.dialect.SUPPORTS_USER_DEFINED_TYPES:
 6637                        this = self._parse_user_defined_type(identifier)
 6638                    else:
 6639                        self._retreat(self._index - 1)
 6640                        return None
 6641            else:
 6642                return None
 6643
 6644        if type_token == TokenType.PSEUDO_TYPE:
 6645            return self.expression(exp.PseudoType(this=self._prev.text.upper()))
 6646
 6647        if type_token == TokenType.OBJECT_IDENTIFIER:
 6648            return self.expression(exp.ObjectIdentifier(this=self._prev.text.upper()))
 6649
 6650        # https://materialize.com/docs/sql/types/map/
 6651        if type_token == TokenType.MAP and self._match(TokenType.L_BRACKET):
 6652            key_type = self._parse_types(
 6653                check_func=check_func, schema=schema, allow_identifiers=allow_identifiers
 6654            )
 6655            if not self._match(TokenType.FARROW):
 6656                self._retreat(index)
 6657                return None
 6658
 6659            value_type = self._parse_types(
 6660                check_func=check_func, schema=schema, allow_identifiers=allow_identifiers
 6661            )
 6662            if not self._match(TokenType.R_BRACKET):
 6663                self._retreat(index)
 6664                return None
 6665
 6666            return exp.DataType(
 6667                this=exp.DType.MAP,
 6668                expressions=[key_type, value_type],
 6669                nested=True,
 6670            )
 6671
 6672        nested = type_token in self.NESTED_TYPE_TOKENS
 6673        is_struct = type_token in self.STRUCT_TYPE_TOKENS
 6674        is_aggregate = type_token in self.AGGREGATE_TYPE_TOKENS
 6675        expressions = None
 6676        maybe_func = False
 6677
 6678        if self._match(TokenType.L_PAREN):
 6679            if is_struct:
 6680                expressions = self._parse_csv(lambda: self._parse_struct_types(type_required=True))
 6681            elif nested:
 6682                expressions = self._parse_csv(
 6683                    lambda: self._parse_types(
 6684                        check_func=check_func, schema=schema, allow_identifiers=allow_identifiers
 6685                    )
 6686                )
 6687                if type_token == TokenType.NULLABLE and len(expressions) == 1:
 6688                    this = expressions[0]
 6689                    this.set("nullable", True)
 6690                    self._match_r_paren()
 6691                    return this
 6692            elif type_token in self.ENUM_TYPE_TOKENS:
 6693                expressions = self._parse_csv(self._parse_equality)
 6694            elif type_token == TokenType.JSON:
 6695                # ClickHouse JSON type supports arguments: JSON(col Type, SKIP col, param=value)
 6696                # https://clickhouse.com/docs/sql-reference/data-types/newjson
 6697                expressions = self._parse_csv(self._parse_json_type_arg)
 6698            elif is_aggregate:
 6699                func_or_ident = self._parse_function(anonymous=True) or self._parse_id_var(
 6700                    any_token=False, tokens=(TokenType.VAR, TokenType.ANY)
 6701                )
 6702                if not func_or_ident:
 6703                    return None
 6704                expressions = [func_or_ident]
 6705                if self._match(TokenType.COMMA):
 6706                    expressions.extend(
 6707                        self._parse_csv(
 6708                            lambda: self._parse_types(
 6709                                check_func=check_func,
 6710                                schema=schema,
 6711                                allow_identifiers=allow_identifiers,
 6712                            )
 6713                        )
 6714                    )
 6715            else:
 6716                expressions = self._parse_csv(self._parse_type_size)
 6717
 6718                # https://docs.snowflake.com/en/sql-reference/data-types-vector
 6719                if type_token == TokenType.VECTOR and len(expressions) == 2:
 6720                    expressions = self._parse_vector_expressions(expressions)
 6721
 6722            if not self._match(TokenType.R_PAREN):
 6723                self._retreat(index)
 6724                return None
 6725
 6726            maybe_func = True
 6727
 6728        values: list[exp.Expr] | None = None
 6729
 6730        if nested and self._match(TokenType.LT):
 6731            if is_struct:
 6732                expressions = self._parse_csv(lambda: self._parse_struct_types(type_required=True))
 6733            else:
 6734                expressions = self._parse_csv(
 6735                    lambda: self._parse_types(
 6736                        check_func=check_func,
 6737                        schema=schema,
 6738                        allow_identifiers=allow_identifiers,
 6739                        with_collation=True,
 6740                    )
 6741                )
 6742
 6743            if not self._match(TokenType.GT):
 6744                self.raise_error("Expecting >")
 6745
 6746            if self._match_set((TokenType.L_BRACKET, TokenType.L_PAREN)):
 6747                values = self._parse_csv(self._parse_disjunction)
 6748                if not values and is_struct:
 6749                    values = None
 6750                    self._retreat(self._index - 1)
 6751                else:
 6752                    self._match_set((TokenType.R_BRACKET, TokenType.R_PAREN))
 6753
 6754        if type_token in self.TIMESTAMPS:
 6755            if self._match_text_seq("WITH", "TIME", "ZONE"):
 6756                maybe_func = False
 6757                tz_type = exp.DType.TIMETZ if type_token in self.TIMES else exp.DType.TIMESTAMPTZ
 6758                this = exp.DataType(this=tz_type, expressions=expressions)
 6759            elif self._match_text_seq("WITH", "LOCAL", "TIME", "ZONE"):
 6760                maybe_func = False
 6761                this = exp.DataType(this=exp.DType.TIMESTAMPLTZ, expressions=expressions)
 6762            elif self._match_text_seq("WITHOUT", "TIME", "ZONE"):
 6763                maybe_func = False
 6764        elif type_token == TokenType.INTERVAL:
 6765            if self._curr.text.upper() in self.dialect.VALID_INTERVAL_UNITS:
 6766                unit = self._parse_var(upper=True)
 6767                if self._match_text_seq("TO"):
 6768                    unit = exp.IntervalSpan(this=unit, expression=self._parse_var(upper=True))
 6769
 6770                this = self.expression(exp.DataType(this=self.expression(exp.Interval(unit=unit))))
 6771            else:
 6772                this = self.expression(exp.DataType(this=exp.DType.INTERVAL))
 6773        elif type_token == TokenType.VOID:
 6774            this = exp.DataType(this=exp.DType.NULL)
 6775
 6776        if maybe_func and check_func:
 6777            index2 = self._index
 6778            peek = self._parse_string()
 6779
 6780            if not peek:
 6781                self._retreat(index)
 6782                return None
 6783
 6784            self._retreat(index2)
 6785
 6786        if not this:
 6787            assert type_token is not None
 6788            if self._match_text_seq("UNSIGNED"):
 6789                unsigned_type_token = self.SIGNED_TO_UNSIGNED_TYPE_TOKEN.get(type_token)
 6790                if not unsigned_type_token:
 6791                    self.raise_error(f"Cannot convert {type_token.name} to unsigned.")
 6792
 6793                type_token = unsigned_type_token or type_token
 6794
 6795            # NULLABLE without parentheses can be a column (Presto/Trino)
 6796            if type_token == TokenType.NULLABLE and not expressions:
 6797                self._retreat(index)
 6798                return None
 6799
 6800            this = exp.DataType(
 6801                this=exp.DType[type_token.name],
 6802                expressions=expressions,
 6803                nested=nested,
 6804            )
 6805
 6806            # Empty arrays/structs are allowed
 6807            if values is not None:
 6808                cls = exp.Struct if is_struct else exp.Array
 6809                this = exp.cast(cls(expressions=values), this, copy=False)
 6810
 6811        elif expressions:
 6812            this.set("expressions", expressions)
 6813
 6814        # https://materialize.com/docs/sql/types/list/#type-name
 6815        while self._match(TokenType.LIST):
 6816            this = exp.DataType(this=exp.DType.LIST, expressions=[this], nested=True)
 6817
 6818        index = self._index
 6819
 6820        # Postgres supports the INT ARRAY[3] syntax as a synonym for INT[3]
 6821        matched_array = self._match(TokenType.ARRAY)
 6822
 6823        while self._curr:
 6824            datatype_token = self._prev.token_type
 6825            matched_l_bracket = self._match(TokenType.L_BRACKET)
 6826
 6827            if (not matched_l_bracket and not matched_array) or (
 6828                datatype_token == TokenType.ARRAY and self._match(TokenType.R_BRACKET)
 6829            ):
 6830                # Postgres allows casting empty arrays such as ARRAY[]::INT[],
 6831                # not to be confused with the fixed size array parsing
 6832                break
 6833
 6834            matched_array = False
 6835            values = self._parse_csv(self._parse_disjunction) or None
 6836            if (
 6837                values
 6838                and not schema
 6839                and (
 6840                    not self.dialect.SUPPORTS_FIXED_SIZE_ARRAYS
 6841                    or datatype_token == TokenType.ARRAY
 6842                    or not self._match(TokenType.R_BRACKET, advance=False)
 6843                )
 6844            ):
 6845                # Retreating here means that we should not parse the following values as part of the data type, e.g. in DuckDB
 6846                # ARRAY[1] should retreat and instead be parsed into exp.Array in contrast to INT[x][y] which denotes a fixed-size array data type
 6847                self._retreat(index)
 6848                break
 6849
 6850            this = exp.DataType(
 6851                this=exp.DType.ARRAY, expressions=[this], values=values, nested=True
 6852            )
 6853            self._match(TokenType.R_BRACKET)
 6854
 6855        if self.TYPE_CONVERTERS and isinstance(this.this, exp.DType):
 6856            converter = self.TYPE_CONVERTERS.get(this.this)
 6857            if converter:
 6858                this = converter(t.cast(exp.DataType, this))
 6859
 6860        if with_collation and isinstance(this, exp.DataType) and self._match(TokenType.COLLATE):
 6861            this.set("collate", self._parse_identifier() or self._parse_column())
 6862
 6863        return this
 6864
 6865    def _parse_json_type_arg(self) -> exp.Expr | None:
 6866        """Parse a single argument to ClickHouse's JSON type."""
 6867
 6868        # SKIP col or SKIP REGEXP 'pattern'
 6869        if self._match_text_seq("SKIP"):
 6870            regexp = self._match(TokenType.RLIKE)
 6871            arg = self._parse_column()
 6872            if isinstance(arg, exp.Column):
 6873                arg = arg.to_dot()
 6874            return self.expression(exp.SkipJSONColumn(regexp=regexp, expression=arg))
 6875
 6876        param_or_col = self._parse_column()
 6877        if not isinstance(param_or_col, exp.Column):
 6878            return None
 6879
 6880        # Parameter: name=value (e.g., max_dynamic_paths=2)
 6881        if len(param_or_col.parts) == 1 and self._match(TokenType.EQ):
 6882            param = param_or_col.name
 6883            value = self._parse_primary()
 6884            return self.expression(exp.EQ(this=exp.var(param), expression=value))
 6885
 6886        # Column type hint: col_name Type
 6887        col = param_or_col.to_dot()
 6888        kind = self._parse_types(check_func=False, allow_identifiers=False)
 6889        return self.expression(exp.ColumnDef(this=col, kind=kind))
 6890
 6891    def _parse_vector_expressions(self, expressions: list[exp.Expr]) -> list[exp.Expr]:
 6892        return [exp.DataType.from_str(expressions[0].name, dialect=self.dialect), *expressions[1:]]
 6893
 6894    def _parse_struct_types(self, type_required: bool = False) -> exp.Expr | None:
 6895        index = self._index
 6896
 6897        if (
 6898            self._curr
 6899            and self._next
 6900            and self._curr.token_type in self.TYPE_TOKENS
 6901            and self._next.token_type in self.TYPE_TOKENS
 6902        ):
 6903            # Takes care of special cases like `STRUCT<list ARRAY<...>>` where the identifier is also a
 6904            # type token. Without this, the list will be parsed as a type and we'll eventually crash
 6905            this = self._parse_id_var()
 6906        else:
 6907            this = (
 6908                self._parse_type(parse_interval=False, fallback_to_identifier=True)
 6909                or self._parse_id_var()
 6910            )
 6911
 6912        self._match(TokenType.COLON)
 6913
 6914        if (
 6915            type_required
 6916            and not isinstance(this, exp.DataType)
 6917            and not self._match_set(self.TYPE_TOKENS, advance=False)
 6918        ):
 6919            self._retreat(index)
 6920            return self._parse_types()
 6921
 6922        return self._parse_column_def(this)
 6923
 6924    def _parse_at_time_zone(self, this: exp.Expr | None) -> exp.Expr | None:
 6925        if not self._match_text_seq("AT", "TIME", "ZONE"):
 6926            return this
 6927        return self._parse_at_time_zone(
 6928            self.expression(exp.AtTimeZone(this=this, zone=self._parse_unary()))
 6929        )
 6930
 6931    def _parse_atom(self) -> exp.Expr | None:
 6932        if (
 6933            self._curr.token_type in self.IDENTIFIER_TOKENS
 6934            and (column := self._parse_column()) is not None
 6935        ):
 6936            return column
 6937
 6938        token = self._curr
 6939        token_type = token.token_type
 6940
 6941        if not (primary_parser := self.PRIMARY_PARSERS.get(token_type)):
 6942            return None
 6943
 6944        next_type = self._next.token_type
 6945
 6946        if (
 6947            next_type in self.COLUMN_OPERATORS
 6948            or next_type in self.COLUMN_POSTFIX_TOKENS
 6949            or (token_type == TokenType.STRING and next_type == TokenType.STRING)
 6950        ):
 6951            return None
 6952
 6953        self._advance()
 6954        return primary_parser(self, token)
 6955
 6956    def _parse_column(self) -> exp.Expr | None:
 6957        column: exp.Expr | None = self._parse_column_parts_fast()
 6958        if column is None:
 6959            this = self._parse_column_reference()
 6960            if not this:
 6961                this = self._parse_bracket(this)
 6962            column = self._parse_column_ops(this) if this else this
 6963
 6964        if column:
 6965            if self.dialect.SUPPORTS_COLUMN_JOIN_MARKS and self._match(TokenType.JOIN_MARKER):
 6966                column.set("join_mark", True)
 6967            if self.COLON_IS_VARIANT_EXTRACT:
 6968                column = self._parse_colon_as_variant_extract(column)
 6969
 6970        return column
 6971
 6972    def _parse_column_parts_fast(self) -> exp.Column | exp.Dot | None:
 6973        """Fast path for simple column and dot references (a, a.b, ...).
 6974
 6975        Greedily consumes VAR/IDENTIFIER tokens separated by DOTs, then checks
 6976        that nothing complex follows. If it does, retreats and returns None so
 6977        the slow path can handle it. For >4 parts, wraps in exp.Dot nodes.
 6978        """
 6979        index = self._index
 6980        parts: list[exp.Identifier] | None = None
 6981        all_comments: list[str] | None = None
 6982
 6983        while self._match_set(self.IDENTIFIER_TOKENS):
 6984            token = self._prev
 6985            comments = self._prev_comments
 6986
 6987            if parts is None and token.text.upper() in self.NO_PAREN_FUNCTION_PARSERS:
 6988                self._retreat(index)
 6989                return None
 6990
 6991            has_dot = self._match(TokenType.DOT)
 6992            curr_tt = self._curr.token_type
 6993
 6994            if not has_dot:
 6995                if curr_tt in self.COLUMN_OPERATORS or curr_tt in self.COLUMN_POSTFIX_TOKENS:
 6996                    self._retreat(index)
 6997                    return None
 6998            elif curr_tt not in self.IDENTIFIER_TOKENS:
 6999                self._retreat(index)
 7000                return None
 7001
 7002            if parts is None:
 7003                parts = []
 7004
 7005            if comments:
 7006                if all_comments is None:
 7007                    all_comments = []
 7008                all_comments.extend(comments)
 7009                self._prev_comments = []
 7010
 7011            parts.append(
 7012                self.expression(
 7013                    exp.Identifier(
 7014                        this=token.text, quoted=token.token_type == TokenType.IDENTIFIER
 7015                    ),
 7016                    token,
 7017                )
 7018            )
 7019
 7020            if not has_dot:
 7021                break
 7022
 7023        if parts is None:
 7024            return None
 7025
 7026        n = len(parts)
 7027
 7028        if n == 1:
 7029            column: exp.Column | exp.Dot = exp.Column(this=parts[0])
 7030        elif n == 2:
 7031            column = exp.Column(this=parts[1], table=parts[0])
 7032        elif n == 3:
 7033            column = exp.Column(this=parts[2], table=parts[1], db=parts[0])
 7034        else:
 7035            column = exp.Column(this=parts[3], table=parts[2], db=parts[1], catalog=parts[0])
 7036
 7037            for i in range(4, n):
 7038                column = exp.Dot(this=column, expression=parts[i])
 7039
 7040        if all_comments:
 7041            column.add_comments(all_comments)
 7042
 7043        return column
 7044
 7045    def _parse_column_reference(self) -> exp.Expr | None:
 7046        this = self._parse_field()
 7047        if (
 7048            not this
 7049            and self._match(TokenType.VALUES, advance=False)
 7050            and self.VALUES_FOLLOWED_BY_PAREN
 7051            and (not self._next or self._next.token_type != TokenType.L_PAREN)
 7052        ):
 7053            this = self._parse_id_var()
 7054
 7055        if isinstance(this, exp.Identifier):
 7056            # We bubble up comments from the Identifier to the Column
 7057            this = self.expression(exp.Column(this=this), comments=this.pop_comments())
 7058
 7059        return this
 7060
 7061    def _build_json_extract(
 7062        self,
 7063        this: exp.Expr | None,
 7064        path_parts: list[exp.JSONPathPart],
 7065    ) -> tuple[exp.Expr | None, list[exp.JSONPathPart]]:
 7066        if len(path_parts) > 1:
 7067            this = self.expression(
 7068                exp.JSONExtract(
 7069                    this=this,
 7070                    expression=exp.JSONPath(expressions=path_parts),
 7071                    variant_extract=True,
 7072                    requires_json=self.JSON_EXTRACT_REQUIRES_JSON_EXPRESSION,
 7073                )
 7074            )
 7075            path_parts = [exp.JSONPathRoot()]
 7076
 7077        return this, path_parts
 7078
 7079    def _parse_colon_as_variant_extract(self, this: exp.Expr | None) -> exp.Expr | None:
 7080        path_parts: list[exp.JSONPathPart] = [exp.JSONPathRoot()]
 7081
 7082        while self._match(TokenType.COLON):
 7083            if not self.COLON_CHAIN_IS_SINGLE_EXTRACT:
 7084                this, path_parts = self._build_json_extract(this, path_parts)
 7085
 7086            key = self._parse_id_var(any_token=True, tokens=(TokenType.SELECT,))
 7087
 7088            if key:
 7089                quoted = isinstance(key, exp.Identifier) and key.quoted
 7090                path_parts.append(exp.JSONPathKey(this=key.name, quoted=quoted))
 7091
 7092            while True:
 7093                if self._match(TokenType.DOT):
 7094                    next_key = self._parse_id_var(any_token=True, tokens=(TokenType.SELECT,))
 7095
 7096                    if next_key:
 7097                        quoted = isinstance(next_key, exp.Identifier) and next_key.quoted
 7098                        path_parts.append(exp.JSONPathKey(this=next_key.name, quoted=quoted))
 7099                elif self._match(TokenType.L_BRACKET):
 7100                    bracket_expr = self._parse_bracket_key_value()
 7101
 7102                    if not self._match(TokenType.R_BRACKET):
 7103                        self.raise_error("Expected ]")
 7104
 7105                    if bracket_expr:
 7106                        if bracket_expr.is_string:
 7107                            path_parts.append(exp.JSONPathKey(this=bracket_expr.name, quoted=True))
 7108                        elif bracket_expr.is_star:
 7109                            path_parts.append(exp.JSONPathSubscript(this=exp.JSONPathWildcard()))
 7110                        elif bracket_expr.is_number:
 7111                            path_parts.append(exp.JSONPathSubscript(this=bracket_expr.to_py()))
 7112                        else:
 7113                            this, path_parts = self._build_json_extract(this, path_parts)
 7114
 7115                            this = self.expression(
 7116                                exp.Bracket(
 7117                                    this=this, expressions=[bracket_expr], json_access=True
 7118                                ),
 7119                            )
 7120
 7121                elif self._match(TokenType.DCOLON):
 7122                    this, path_parts = self._build_json_extract(this, path_parts)
 7123
 7124                    cast_type = self._parse_types()
 7125                    if cast_type:
 7126                        this = self.expression(exp.Cast(this=this, to=cast_type))
 7127                    else:
 7128                        self.raise_error("Expected type after '::'")
 7129                else:
 7130                    break
 7131
 7132        this, _ = self._build_json_extract(this, path_parts)
 7133
 7134        return this
 7135
 7136    def _parse_dcolon(self) -> exp.Expr | None:
 7137        return self._parse_types()
 7138
 7139    def _parse_column_ops(self, this: exp.Expr | None) -> exp.Expr | None:
 7140        while self._curr.token_type in self.BRACKETS:
 7141            this = self._parse_bracket(this)
 7142
 7143        column_operators = self.COLUMN_OPERATORS
 7144        cast_column_operators = self.CAST_COLUMN_OPERATORS
 7145        while self._curr:
 7146            op_token = self._curr.token_type
 7147
 7148            if op_token not in column_operators:
 7149                break
 7150            op = column_operators[op_token]
 7151            self._advance()
 7152
 7153            if op_token in cast_column_operators:
 7154                field = self._parse_dcolon()
 7155                if not field:
 7156                    self.raise_error("Expected type")
 7157            elif op and self._curr:
 7158                field = self._parse_column_reference() or self._parse_bitwise()
 7159                if isinstance(field, exp.Column) and self._match(TokenType.DOT, advance=False):
 7160                    field = self._parse_column_ops(field)
 7161            else:
 7162                dot = self._is_connected() and self._prev.token_type == TokenType.DOT
 7163                field = self._parse_field(any_token=True, anonymous_func=True)
 7164
 7165                # In t.true, t.null we should produce an Identifier node
 7166                if dot and isinstance(field, (exp.Null, exp.Boolean)):
 7167                    field = self.expression(
 7168                        exp.Identifier(this=self._prev.text),
 7169                        comments=field.comments,
 7170                    )
 7171
 7172            # Function calls can be qualified, e.g., x.y.FOO()
 7173            # This converts the final AST to a series of Dots leading to the function call
 7174            # https://cloud.google.com/bigquery/docs/reference/standard-sql/functions-reference#function_call_rules
 7175            if isinstance(field, (exp.Func, exp.Window)) and this:
 7176                this = this.transform(
 7177                    lambda n: n.to_dot(include_dots=False) if isinstance(n, exp.Column) else n
 7178                )
 7179
 7180            if op:
 7181                this = op(self, this, field)
 7182            elif isinstance(this, exp.Column) and not this.args.get("catalog"):
 7183                this = self.expression(
 7184                    exp.Column(
 7185                        this=field,
 7186                        table=this.this,
 7187                        db=this.args.get("table"),
 7188                        catalog=this.args.get("db"),
 7189                    ),
 7190                    comments=this.comments,
 7191                )
 7192            elif isinstance(field, exp.Window):
 7193                # Move the exp.Dot's to the window's function
 7194                window_func = self.expression(exp.Dot(this=this, expression=field.this))
 7195                field.set("this", window_func)
 7196                this = field
 7197            else:
 7198                this = self.expression(exp.Dot(this=this, expression=field))
 7199
 7200            if field and field.comments:
 7201                t.cast(exp.Expr, this).add_comments(field.pop_comments())
 7202
 7203            this = self._parse_bracket(this)
 7204
 7205        return this
 7206
 7207    def _parse_paren(self) -> exp.Expr | None:
 7208        if not self._match(TokenType.L_PAREN):
 7209            return None
 7210
 7211        comments = self._prev_comments
 7212        query = self._parse_select()
 7213
 7214        if query:
 7215            expressions = [query]
 7216        else:
 7217            expressions = self._parse_expressions()
 7218
 7219        this = seq_get(expressions, 0)
 7220
 7221        if not this and self._match(TokenType.R_PAREN, advance=False):
 7222            this = self.expression(exp.Tuple())
 7223        elif len(expressions) > 1 or self._prev.token_type == TokenType.COMMA:
 7224            this = self.expression(exp.Tuple(expressions=expressions))
 7225        elif isinstance(this, exp.UNWRAPPED_QUERIES):
 7226            this = self._parse_subquery(this=this, parse_alias=False)
 7227        elif isinstance(this, (exp.Subquery, exp.Values)):
 7228            this = self._parse_subquery(
 7229                this=self._parse_query_modifiers(self._parse_set_operations(this)),
 7230                parse_alias=False,
 7231            )
 7232        else:
 7233            this = self.expression(exp.Paren(this=this))
 7234
 7235        if this:
 7236            this.add_comments(comments)
 7237
 7238        self._match_r_paren(expression=this)
 7239
 7240        if isinstance(this, exp.Paren) and isinstance(this.this, exp.AggFunc):
 7241            return self._parse_window(this)
 7242
 7243        return this
 7244
 7245    def _parse_primary(self) -> exp.Expr | None:
 7246        if self._match_set(self.PRIMARY_PARSERS):
 7247            token_type = self._prev.token_type
 7248            primary = self.PRIMARY_PARSERS[token_type](self, self._prev)
 7249
 7250            if token_type == TokenType.STRING:
 7251                expressions = [primary]
 7252                while self._match(TokenType.STRING, advance=False):
 7253                    if self._is_connected() and self.ADJACENT_STRINGS_CANNOT_BE_CONNECTED:
 7254                        self.raise_error(
 7255                            "Adjacent string literals need to be separated by whitespace or comments"
 7256                        )
 7257
 7258                    self._advance()
 7259                    expressions.append(exp.Literal.string(self._prev.text))
 7260
 7261                if len(expressions) > 1:
 7262                    return self.expression(
 7263                        exp.Concat(expressions=expressions, coalesce=self.dialect.CONCAT_COALESCE)
 7264                    )
 7265
 7266            return primary
 7267
 7268        if self._match_pair(TokenType.DOT, TokenType.NUMBER):
 7269            return exp.Literal.number(f"0.{self._prev.text}")
 7270
 7271        return self._parse_paren()
 7272
 7273    def _parse_field(
 7274        self,
 7275        any_token: bool = False,
 7276        tokens: t.Collection[TokenType] | None = None,
 7277        anonymous_func: bool = False,
 7278    ) -> exp.Expr | None:
 7279        after_dot = (
 7280            self.SUPPORTS_DIGIT_PREFIXED_FIELD_NAMES and self._prev.token_type == TokenType.DOT
 7281        )
 7282
 7283        if anonymous_func:
 7284            field = (
 7285                self._parse_function(anonymous=anonymous_func, any_token=any_token)
 7286                or self._parse_primary()
 7287            )
 7288        else:
 7289            field = self._parse_primary() or self._parse_function(
 7290                anonymous=anonymous_func, any_token=any_token
 7291            )
 7292
 7293        field = field or self._parse_id_var(any_token=any_token, tokens=tokens)
 7294
 7295        if after_dot and isinstance(field, exp.Literal) and field.is_number:
 7296            name = field.name
 7297            if self._is_connected() and self._parse_var(any_token=True):
 7298                name += self._prev.text
 7299
 7300            field = exp.Identifier(this=name, quoted=True).update_positions(field)
 7301
 7302        return field
 7303
 7304    def _parse_function(
 7305        self,
 7306        functions: dict[str, t.Callable] | None = None,
 7307        anonymous: bool = False,
 7308        optional_parens: bool = True,
 7309        any_token: bool = False,
 7310    ) -> exp.Expr | None:
 7311        # This allows us to also parse {fn <function>} syntax (Snowflake, MySQL support this)
 7312        # See: https://community.snowflake.com/s/article/SQL-Escape-Sequences
 7313        fn_syntax = False
 7314        if (
 7315            self._match(TokenType.L_BRACE, advance=False)
 7316            and self._next
 7317            and self._next.text.upper() == "FN"
 7318        ):
 7319            self._advance(2)
 7320            fn_syntax = True
 7321
 7322        func = self._parse_function_call(
 7323            functions=functions,
 7324            anonymous=anonymous,
 7325            optional_parens=optional_parens,
 7326            any_token=any_token,
 7327        )
 7328
 7329        if fn_syntax:
 7330            self._match(TokenType.R_BRACE)
 7331
 7332        return func
 7333
 7334    def _parse_function_args(self, alias: bool = False) -> list[exp.Expr]:
 7335        return self._parse_csv(lambda: self._parse_lambda(alias=alias))
 7336
 7337    def _parse_connector_function(self, connector: t.Callable[..., exp.Condition]) -> exp.Paren:
 7338        args = self._parse_function_args(alias=False)
 7339        if not args:
 7340            self.raise_error("Expected at least one argument")
 7341
 7342        # Wrapped so the connector keeps its precedence in the parent context
 7343        return exp.Paren(this=connector(*args, copy=False))
 7344
 7345    def _parse_function_call(
 7346        self,
 7347        functions: dict[str, t.Callable] | None = None,
 7348        anonymous: bool = False,
 7349        optional_parens: bool = True,
 7350        any_token: bool = False,
 7351    ) -> exp.Expr | None:
 7352        if not self._curr:
 7353            return None
 7354
 7355        comments = self._curr.comments
 7356        prev = self._prev
 7357        token = self._curr
 7358        token_type = self._curr.token_type
 7359        this: str | exp.Expr = self._curr.text
 7360        upper = self._curr.text.upper()
 7361
 7362        after_dot = prev.token_type == TokenType.DOT
 7363        parser = self.NO_PAREN_FUNCTION_PARSERS.get(upper)
 7364        if (
 7365            optional_parens
 7366            and parser
 7367            and token_type not in self.INVALID_FUNC_NAME_TOKENS
 7368            and not after_dot
 7369        ):
 7370            self._advance()
 7371            return self._parse_window(parser(self))
 7372
 7373        if self._next.token_type != TokenType.L_PAREN:
 7374            if optional_parens and token_type in self.NO_PAREN_FUNCTIONS and not after_dot:
 7375                self._advance()
 7376                return self.expression(self.NO_PAREN_FUNCTIONS[token_type]())
 7377
 7378            return None
 7379
 7380        if any_token:
 7381            if token_type in self.RESERVED_TOKENS:
 7382                return None
 7383        elif token_type not in self.FUNC_TOKENS:
 7384            return None
 7385
 7386        self._advance(2)
 7387
 7388        parser = self.FUNCTION_PARSERS.get(upper)
 7389        if parser and not anonymous:
 7390            result = parser(self)
 7391        else:
 7392            subquery_predicate = self.SUBQUERY_PREDICATES.get(token_type)
 7393
 7394            if subquery_predicate:
 7395                expr = None
 7396                if self._curr.token_type in self.SUBQUERY_TOKENS:
 7397                    expr = self._parse_select()
 7398                    self._match_r_paren()
 7399                elif prev and prev.token_type in (TokenType.LIKE, TokenType.ILIKE):
 7400                    # Backtrack one token since we've consumed the L_PAREN here. Instead, we'd like
 7401                    # to parse "LIKE [ANY | ALL] (...)" as a whole into an exp.Tuple or exp.Paren
 7402                    self._advance(-1)
 7403                    expr = self._parse_bitwise()
 7404
 7405                if expr:
 7406                    return self.expression(subquery_predicate(this=expr), comments=comments)
 7407
 7408            if functions is None:
 7409                functions = self.FUNCTIONS
 7410
 7411            function = functions.get(upper)
 7412            known_function = function and not anonymous
 7413
 7414            alias = not known_function or upper in self.FUNCTIONS_WITH_ALIASED_ARGS
 7415            args = self._parse_function_args(alias)
 7416
 7417            post_func_comments = self._curr.comments if self._curr else None
 7418            if known_function and post_func_comments:
 7419                # If the user-inputted comment "/* sqlglot.anonymous */" is following the function
 7420                # call we'll construct it as exp.Anonymous, even if it's "known"
 7421                if any(
 7422                    comment.lstrip().startswith(exp.SQLGLOT_ANONYMOUS)
 7423                    for comment in post_func_comments
 7424                ):
 7425                    known_function = False
 7426
 7427            if alias and known_function:
 7428                args = self._kv_to_prop_eq(args)
 7429
 7430            if known_function:
 7431                func_builder = t.cast(t.Callable, function)
 7432
 7433                # mypyc compiled functions don't have __code__, so we use
 7434                # try/except to check if func_builder accepts 'dialect'.
 7435                try:
 7436                    func = func_builder(args)
 7437                except TypeError:
 7438                    func = func_builder(args, dialect=self.dialect)
 7439
 7440                func = self.validate_expression(func, args)
 7441                if name_key := self.dialect.ORIGINAL_NAME_META_KEY:
 7442                    func.meta[name_key] = this
 7443
 7444                result = func
 7445            else:
 7446                if token_type == TokenType.IDENTIFIER:
 7447                    this = exp.Identifier(this=this, quoted=True).update_positions(token)
 7448
 7449                result = self.expression(exp.Anonymous(this=this, expressions=args))
 7450
 7451            result = result.update_positions(token)
 7452
 7453        if isinstance(result, exp.Expr):
 7454            result.add_comments(comments)
 7455
 7456        if parser:
 7457            self._match(TokenType.R_PAREN, expression=result)
 7458        else:
 7459            self._match_r_paren(result)
 7460        return self._parse_window(result)
 7461
 7462    def _to_prop_eq(self, expression: exp.Expr, index: int) -> exp.Expr:
 7463        return expression
 7464
 7465    def _kv_to_prop_eq(
 7466        self, expressions: list[exp.Expr], parse_map: bool = False
 7467    ) -> list[exp.Expr]:
 7468        transformed = []
 7469
 7470        for index, e in enumerate(expressions):
 7471            if isinstance(e, self.KEY_VALUE_DEFINITIONS):
 7472                if isinstance(e, exp.Alias):
 7473                    e = self.expression(exp.PropertyEQ(this=e.args.get("alias"), expression=e.this))
 7474
 7475                if not isinstance(e, exp.PropertyEQ):
 7476                    e = self.expression(
 7477                        exp.PropertyEQ(
 7478                            this=e.this if parse_map else exp.to_identifier(e.this.name),
 7479                            expression=e.expression,
 7480                        )
 7481                    )
 7482
 7483                if isinstance(e.this, exp.Column):
 7484                    e.this.replace(e.this.this)
 7485            else:
 7486                e = self._to_prop_eq(e, index)
 7487
 7488            transformed.append(e)
 7489
 7490        return transformed
 7491
 7492    def _parse_function_properties(self) -> exp.Properties | None:
 7493        # Skip the generic `key = value` fallback in _parse_property since this
 7494        # runs post-AS where a function body like `name = expr` can be misread
 7495        # as a property.
 7496        properties = []
 7497        while True:
 7498            if self._match_texts(self.PROPERTY_PARSERS):
 7499                keyword = self._prev.text.upper()
 7500                prop = self.PROPERTY_PARSERS[keyword](self)
 7501            elif self._match(TokenType.DEFAULT) and self._match_texts(self.PROPERTY_PARSERS):
 7502                keyword = self._prev.text.upper()
 7503                prop = self.PROPERTY_PARSERS[keyword](self, default=True)
 7504            else:
 7505                break
 7506            if not prop:
 7507                self.raise_error(f"Failed to parse property '{keyword}'")
 7508                break
 7509            for p in ensure_list(prop):
 7510                properties.append(p)
 7511
 7512        return self.expression(exp.Properties(expressions=properties)) if properties else None
 7513
 7514    def _parse_user_defined_function_expression(self) -> exp.Expr | None:
 7515        return self._parse_statement()
 7516
 7517    def _parse_function_parameter(self) -> exp.Expr | None:
 7518        return self._parse_column_def(this=self._parse_id_var(), computed_column=False)
 7519
 7520    def _parse_user_defined_function(self, kind: TokenType | None = None) -> exp.Expr | None:
 7521        this = self._parse_table_parts(schema=True)
 7522
 7523        if not self._match(TokenType.L_PAREN):
 7524            return this
 7525
 7526        expressions = self._parse_csv(self._parse_function_parameter)
 7527        self._match_r_paren()
 7528        return self.expression(
 7529            exp.UserDefinedFunction(this=this, expressions=expressions, wrapped=True)
 7530        )
 7531
 7532    def _parse_macro_overloads(
 7533        self,
 7534        this: exp.UserDefinedFunction,
 7535        first_body: exp.Expr,
 7536        first_is_table: bool = False,
 7537    ) -> exp.MacroOverloads:
 7538        overloads = [
 7539            self.expression(
 7540                exp.MacroOverload(
 7541                    this=first_body,
 7542                    expressions=this.expressions or None,
 7543                    is_table=first_is_table,
 7544                )
 7545            )
 7546        ]
 7547        this.set("expressions", None)
 7548        this.set("wrapped", False)
 7549
 7550        while self._match(TokenType.COMMA):
 7551            if not self._match(TokenType.L_PAREN):
 7552                break
 7553
 7554            params = self._parse_csv(self._parse_function_parameter)
 7555            self._match_r_paren()
 7556
 7557            if not self._match(TokenType.ALIAS):
 7558                break
 7559
 7560            is_table = self._match(TokenType.TABLE)
 7561            body = self._parse_expression()
 7562            macro = exp.MacroOverload(this=body, expressions=params, is_table=is_table)
 7563            overloads.append(self.expression(macro))
 7564
 7565        return self.expression(exp.MacroOverloads(expressions=overloads))
 7566
 7567    def _parse_introducer(self, token: Token) -> exp.Introducer | exp.Identifier:
 7568        literal = self._parse_primary()
 7569        if literal:
 7570            return self.expression(exp.Introducer(this=token.text, expression=literal), token)
 7571
 7572        return self._identifier_expression(token)
 7573
 7574    def _parse_session_parameter(self) -> exp.SessionParameter:
 7575        kind = None
 7576        this = self._parse_id_var() or self._parse_primary()
 7577
 7578        if this and self._match(TokenType.DOT):
 7579            kind = this.name
 7580            this = self._parse_var() or self._parse_primary()
 7581
 7582        return self.expression(exp.SessionParameter(this=this, kind=kind))
 7583
 7584    def _parse_lambda_arg(self) -> exp.Expr | None:
 7585        return self._parse_id_var()
 7586
 7587    def _parse_lambda(self, alias: bool = False) -> exp.Expr | None:
 7588        next_token_type = self._next.token_type
 7589
 7590        # Fast path: simple atom (column, literal, null, bool) followed by , or )
 7591        if (
 7592            next_token_type in self.LAMBDA_ARG_TERMINATORS
 7593            and (atom := self._parse_atom()) is not None
 7594        ):
 7595            return atom
 7596
 7597        index = self._index
 7598
 7599        if self._match(TokenType.L_PAREN):
 7600            expressions = t.cast(
 7601                list[t.Optional[exp.Expr]], self._parse_csv(self._parse_lambda_arg)
 7602            )
 7603
 7604            if not self._match(TokenType.R_PAREN):
 7605                self._retreat(index)
 7606            elif self._match_set(self.LAMBDAS):
 7607                return self.LAMBDAS[self._prev.token_type](self, expressions)
 7608            else:
 7609                self._retreat(index)
 7610        elif self.TYPED_LAMBDA_ARGS or next_token_type in self.LAMBDAS:
 7611            expressions = [self._parse_lambda_arg()]
 7612
 7613            if self._match_set(self.LAMBDAS):
 7614                return self.LAMBDAS[self._prev.token_type](self, expressions)
 7615
 7616            self._retreat(index)
 7617
 7618        this: exp.Expr | None
 7619
 7620        if self._match(TokenType.DISTINCT):
 7621            this = self.expression(
 7622                exp.Distinct(expressions=self._parse_csv(self._parse_disjunction))
 7623            )
 7624        else:
 7625            self._match(TokenType.ALL)  # ALL is the default/no-op aggregate modifier (SQL-92)
 7626            this = self._parse_select_or_expression(alias=alias)
 7627
 7628        return self._parse_limit(
 7629            self._parse_respect_or_ignore_nulls(
 7630                self._parse_order(self._parse_having_max(self._parse_respect_or_ignore_nulls(this)))
 7631            )
 7632        )
 7633
 7634    def _parse_schema(self, this: exp.Expr | None = None) -> exp.Expr | None:
 7635        index = self._index
 7636        if not self._match(TokenType.L_PAREN):
 7637            return this
 7638
 7639        # Disambiguate between schema and subquery/CTE, e.g. in INSERT INTO table (<expr>),
 7640        # expr can be of both types
 7641        if self._match_set(self.SELECT_START_TOKENS):
 7642            self._retreat(index)
 7643            return this
 7644        args = self._parse_csv(lambda: self._parse_constraint() or self._parse_field_def())
 7645        self._match_r_paren()
 7646        return self.expression(exp.Schema(this=this, expressions=args))
 7647
 7648    def _parse_field_def(self) -> exp.Expr | None:
 7649        return self._parse_column_def(self._parse_field(any_token=True))
 7650
 7651    def _parse_column_def(
 7652        self, this: exp.Expr | None, computed_column: bool = True
 7653    ) -> exp.Expr | None:
 7654        # column defs are not really columns, they're identifiers
 7655        if isinstance(this, exp.Column):
 7656            this = this.this
 7657
 7658        if not computed_column:
 7659            self._match(TokenType.ALIAS)
 7660
 7661        kind = self._parse_types(schema=True)
 7662
 7663        if self._match_text_seq("FOR", "ORDINALITY"):
 7664            return self.expression(exp.ColumnDef(this=this, ordinality=True))
 7665
 7666        constraints: list[exp.Expr] = []
 7667
 7668        if (not kind and self._match(TokenType.ALIAS)) or self._match_texts(
 7669            ("ALIAS", "MATERIALIZED")
 7670        ):
 7671            # Match storage before _parse_types so STORED is not treated as a data type
 7672            # (needed for typeless columns, e.g. SQLite `b AS (a * 2) STORED`).
 7673            persisted = self._prev.text.upper() == "MATERIALIZED"
 7674            expression = self._parse_disjunction()
 7675            if not persisted:
 7676                if self._match_text_seq("PERSISTED"):
 7677                    persisted = True
 7678                elif self._match_texts(("STORED", "VIRTUAL")):
 7679                    persisted = self._prev.text.upper() == "STORED"
 7680            constraint_kind = exp.ComputedColumnConstraint(
 7681                this=expression,
 7682                persisted=persisted,
 7683                data_type=exp.Var(this="AUTO")
 7684                if self._match_text_seq("AUTO")
 7685                else self._parse_types(),
 7686                not_null=self._match_pair(TokenType.NOT, TokenType.NULL),
 7687            )
 7688            constraints.append(self.expression(exp.ColumnConstraint(kind=constraint_kind)))
 7689        elif not kind and self._match_set({TokenType.IN, TokenType.OUT}, advance=False):
 7690            in_out_constraint = self.expression(
 7691                exp.InOutColumnConstraint(
 7692                    input_=self._match(TokenType.IN), output=self._match(TokenType.OUT)
 7693                )
 7694            )
 7695            constraints.append(in_out_constraint)
 7696            kind = self._parse_types()
 7697        elif (
 7698            kind
 7699            and self._match(TokenType.ALIAS, advance=False)
 7700            and (
 7701                not self.WRAPPED_TRANSFORM_COLUMN_CONSTRAINT
 7702                or self._next.token_type == TokenType.L_PAREN
 7703            )
 7704        ):
 7705            self._advance()
 7706            constraints.append(
 7707                self.expression(
 7708                    exp.ColumnConstraint(
 7709                        kind=exp.ComputedColumnConstraint(
 7710                            this=self._parse_disjunction(),
 7711                            persisted=self._match_texts(("STORED", "VIRTUAL"))
 7712                            and self._prev.text.upper() == "STORED",
 7713                        )
 7714                    )
 7715                )
 7716            )
 7717
 7718        while True:
 7719            constraint = self._parse_column_constraint()
 7720            if not constraint:
 7721                break
 7722            constraints.append(constraint)
 7723
 7724        if not kind and not constraints:
 7725            return this
 7726
 7727        position = None
 7728        if self._match_texts(("FIRST", "AFTER")):
 7729            pos = self._prev.text
 7730            position = self.expression(exp.ColumnPosition(this=self._parse_column(), position=pos))
 7731
 7732        return self.expression(
 7733            exp.ColumnDef(this=this, kind=kind, constraints=constraints, position=position)
 7734        )
 7735
 7736    def _parse_auto_increment(
 7737        self,
 7738    ) -> exp.GeneratedAsIdentityColumnConstraint | exp.AutoIncrementColumnConstraint:
 7739        start = None
 7740        increment = None
 7741        order = None
 7742
 7743        if self._match(TokenType.L_PAREN, advance=False):
 7744            args = self._parse_wrapped_csv(self._parse_bitwise)
 7745            start = seq_get(args, 0)
 7746            increment = seq_get(args, 1)
 7747
 7748        # The remaining parts form an unordered bag and any of them can be omitted, in which
 7749        # case the engine falls back to its own default, so they're parsed independently.
 7750        while True:
 7751            if self._match_text_seq("START"):
 7752                start = self._parse_bitwise()
 7753            elif self._match_text_seq("INCREMENT"):
 7754                increment = self._parse_bitwise()
 7755            elif self._match_text_seq("ORDER"):
 7756                order = True
 7757            elif self._match_text_seq("NOORDER"):
 7758                order = False
 7759            else:
 7760                break
 7761
 7762        if start or increment or order is not None:
 7763            return exp.GeneratedAsIdentityColumnConstraint(
 7764                start=start, increment=increment, this=False, order=order
 7765            )
 7766
 7767        return exp.AutoIncrementColumnConstraint()
 7768
 7769    def _parse_check_constraint(self) -> exp.CheckColumnConstraint | None:
 7770        if not self._match(TokenType.L_PAREN, advance=False):
 7771            return None
 7772
 7773        return self.expression(
 7774            exp.CheckColumnConstraint(
 7775                this=self._parse_wrapped(self._parse_assignment),
 7776                enforced=self._match_text_seq("ENFORCED"),
 7777            )
 7778        )
 7779
 7780    def _parse_auto_property(self) -> exp.AutoRefreshProperty | None:
 7781        if not self._match_text_seq("REFRESH"):
 7782            self._retreat(self._index - 1)
 7783            return None
 7784        return self.expression(exp.AutoRefreshProperty(this=self._parse_var(upper=True)))
 7785
 7786    def _parse_compress(self) -> exp.CompressColumnConstraint:
 7787        if self._match(TokenType.L_PAREN, advance=False):
 7788            return self.expression(
 7789                exp.CompressColumnConstraint(this=self._parse_wrapped_csv(self._parse_bitwise))
 7790            )
 7791
 7792        return self.expression(exp.CompressColumnConstraint(this=self._parse_bitwise()))
 7793
 7794    def _parse_generated_as_identity(
 7795        self,
 7796    ) -> (
 7797        exp.GeneratedAsIdentityColumnConstraint
 7798        | exp.ComputedColumnConstraint
 7799        | exp.GeneratedAsRowColumnConstraint
 7800    ):
 7801        if self._match_text_seq("BY", "DEFAULT"):
 7802            on_null = self._match_pair(TokenType.ON, TokenType.NULL)
 7803            this = self.expression(
 7804                exp.GeneratedAsIdentityColumnConstraint(this=False, on_null=on_null)
 7805            )
 7806        else:
 7807            self._match_text_seq("ALWAYS")
 7808            this = self.expression(exp.GeneratedAsIdentityColumnConstraint(this=True))
 7809
 7810        self._match(TokenType.ALIAS)
 7811
 7812        if self._match_text_seq("ROW"):
 7813            start = self._match_text_seq("START")
 7814            if not start:
 7815                self._match(TokenType.END)
 7816            hidden = self._match_text_seq("HIDDEN")
 7817            return self.expression(exp.GeneratedAsRowColumnConstraint(start=start, hidden=hidden))
 7818
 7819        identity = self._match_text_seq("IDENTITY")
 7820
 7821        if self._match(TokenType.L_PAREN):
 7822            if self._match_text_seq("START", "WITH"):
 7823                this.set("start", self._parse_bitwise())
 7824            if self._match_text_seq("INCREMENT", "BY"):
 7825                this.set("increment", self._parse_bitwise())
 7826            if self._match_text_seq("MINVALUE"):
 7827                this.set("minvalue", self._parse_bitwise())
 7828            if self._match_text_seq("MAXVALUE"):
 7829                this.set("maxvalue", self._parse_bitwise())
 7830
 7831            if self._match_text_seq("CYCLE"):
 7832                this.set("cycle", True)
 7833            elif self._match_text_seq("NO", "CYCLE"):
 7834                this.set("cycle", False)
 7835
 7836            if not identity:
 7837                this.set("expression", self._parse_range())
 7838            elif not this.args.get("start") and self._match(TokenType.NUMBER, advance=False):
 7839                args = self._parse_csv(self._parse_bitwise)
 7840                this.set("start", seq_get(args, 0))
 7841                this.set("increment", seq_get(args, 1))
 7842
 7843            self._match_r_paren()
 7844
 7845        return this
 7846
 7847    def _parse_inline(self) -> exp.InlineLengthColumnConstraint:
 7848        self._match_text_seq("LENGTH")
 7849        return self.expression(exp.InlineLengthColumnConstraint(this=self._parse_bitwise()))
 7850
 7851    def _parse_not_constraint(self) -> exp.Expr | None:
 7852        if self._match_text_seq("NULL"):
 7853            return self.expression(exp.NotNullColumnConstraint())
 7854        if self._match_text_seq("CASESPECIFIC"):
 7855            return self.expression(exp.CaseSpecificColumnConstraint(not_=True))
 7856        if self._match_text_seq("FOR", "REPLICATION"):
 7857            return self.expression(exp.NotForReplicationColumnConstraint())
 7858
 7859        # Unconsume the `NOT` token
 7860        self._retreat(self._index - 1)
 7861        return None
 7862
 7863    def _parse_column_constraint(self) -> exp.Expr | None:
 7864        this = self._parse_id_var() if self._match(TokenType.CONSTRAINT) else None
 7865
 7866        procedure_option_follows = (
 7867            self._match(TokenType.WITH, advance=False)
 7868            and self._next
 7869            and self._next.text.upper() in self.PROCEDURE_OPTIONS
 7870        )
 7871
 7872        index = self._index
 7873        if not procedure_option_follows and self._match_texts(self.CONSTRAINT_PARSERS):
 7874            constraint = self.CONSTRAINT_PARSERS[self._prev.text.upper()](self)
 7875            if not constraint:
 7876                self._retreat(index)
 7877                return None
 7878
 7879            return self.expression(exp.ColumnConstraint(this=this, kind=constraint))
 7880
 7881        if self._match(TokenType.CHARACTER_SET) or self._match_pair(TokenType.CHAR, TokenType.SET):
 7882            return self.expression(
 7883                exp.ColumnConstraint(
 7884                    this=this,
 7885                    kind=self.expression(
 7886                        exp.CharacterSetColumnConstraint(this=self._parse_var_or_string())
 7887                    ),
 7888                )
 7889            )
 7890
 7891        return this
 7892
 7893    def _parse_constraint(self) -> exp.Expr | None:
 7894        if not self._match(TokenType.CONSTRAINT):
 7895            return self._parse_unnamed_constraint(constraints=self.SCHEMA_UNNAMED_CONSTRAINTS)
 7896
 7897        return self.expression(
 7898            exp.Constraint(this=self._parse_id_var(), expressions=self._parse_unnamed_constraints())
 7899        )
 7900
 7901    def _parse_unnamed_constraints(self) -> list[exp.Expr]:
 7902        constraints = []
 7903        while True:
 7904            constraint = self._parse_unnamed_constraint() or self._parse_function()
 7905            if not constraint:
 7906                break
 7907            constraints.append(constraint)
 7908
 7909        return constraints
 7910
 7911    def _parse_unnamed_constraint(self, constraints: TEXTS_TYPE | None = None) -> exp.Expr | None:
 7912        index = self._index
 7913
 7914        if self._match(TokenType.IDENTIFIER, advance=False) or not self._match_texts(
 7915            constraints or self.CONSTRAINT_PARSERS
 7916        ):
 7917            return None
 7918
 7919        constraint_key = self._prev.text.upper()
 7920        if constraint_key not in self.CONSTRAINT_PARSERS:
 7921            self.raise_error(f"No parser found for schema constraint {constraint_key}.")
 7922
 7923        result = self.CONSTRAINT_PARSERS[constraint_key](self)
 7924        if not result:
 7925            self._retreat(index)
 7926
 7927        return result
 7928
 7929    def _parse_unique_key(self) -> exp.Expr | None:
 7930        if (
 7931            self._curr
 7932            and self._curr.token_type != TokenType.IDENTIFIER
 7933            and self._curr.text.upper() in self.CONSTRAINT_PARSERS
 7934        ):
 7935            return None
 7936        return self._parse_id_var(any_token=False)
 7937
 7938    def _parse_unique(self) -> exp.UniqueColumnConstraint:
 7939        self._match_texts(("KEY", "INDEX"))
 7940        return self.expression(
 7941            exp.UniqueColumnConstraint(
 7942                nulls=self._match_text_seq("NULLS", "NOT", "DISTINCT"),
 7943                this=self._parse_schema(self._parse_unique_key()),
 7944                index_type=self._match(TokenType.USING) and self._advance_any() and self._prev.text,
 7945                on_conflict=self._parse_on_conflict(),
 7946                options=self._parse_key_constraint_options(),
 7947            )
 7948        )
 7949
 7950    def _parse_key_constraint_options(self) -> list[str]:
 7951        options = []
 7952        while True:
 7953            if not self._curr:
 7954                break
 7955
 7956            if self._match(TokenType.ON):
 7957                action = None
 7958                on = self._advance_any() and self._prev.text
 7959
 7960                if self._match_text_seq("NO", "ACTION"):
 7961                    action = "NO ACTION"
 7962                elif self._match_text_seq("CASCADE"):
 7963                    action = "CASCADE"
 7964                elif self._match_text_seq("RESTRICT"):
 7965                    action = "RESTRICT"
 7966                elif self._match_pair(TokenType.SET, TokenType.NULL):
 7967                    action = "SET NULL"
 7968                elif self._match_pair(TokenType.SET, TokenType.DEFAULT):
 7969                    action = "SET DEFAULT"
 7970                else:
 7971                    self.raise_error("Invalid key constraint")
 7972
 7973                options.append(f"ON {on} {action}")
 7974            else:
 7975                var = self._parse_var_from_options(
 7976                    self.KEY_CONSTRAINT_OPTIONS, raise_unmatched=False
 7977                )
 7978                if not var:
 7979                    break
 7980                options.append(var.name)
 7981
 7982        return options
 7983
 7984    def _parse_references(self, match: bool = True) -> exp.Reference | None:
 7985        if match and not self._match(TokenType.REFERENCES):
 7986            return None
 7987
 7988        expressions: list | None = None
 7989        this = self._parse_table(schema=True)
 7990        options = self._parse_key_constraint_options()
 7991        return self.expression(exp.Reference(this=this, expressions=expressions, options=options))
 7992
 7993    def _parse_foreign_key(self) -> exp.ForeignKey:
 7994        expressions = (
 7995            self._parse_wrapped_id_vars()
 7996            if not self._match(TokenType.REFERENCES, advance=False)
 7997            else None
 7998        )
 7999        reference = self._parse_references()
 8000        on_options = {}
 8001
 8002        while self._match(TokenType.ON):
 8003            if not self._match_set((TokenType.DELETE, TokenType.UPDATE)):
 8004                self.raise_error("Expected DELETE or UPDATE")
 8005
 8006            kind = self._prev.text.lower()
 8007
 8008            if self._match_text_seq("NO", "ACTION"):
 8009                action = "NO ACTION"
 8010            elif self._match(TokenType.SET):
 8011                self._match_set((TokenType.NULL, TokenType.DEFAULT))
 8012                action = "SET " + self._prev.text.upper()
 8013            else:
 8014                self._advance()
 8015                action = self._prev.text.upper()
 8016
 8017            on_options[kind] = action
 8018
 8019        return self.expression(
 8020            exp.ForeignKey(
 8021                expressions=expressions,
 8022                reference=reference,
 8023                options=self._parse_key_constraint_options(),
 8024                **on_options,
 8025            )
 8026        )
 8027
 8028    def _parse_primary_key_part(self) -> exp.Expr | None:
 8029        return self._parse_field()
 8030
 8031    def _parse_period_for_system_time(self) -> exp.PeriodForSystemTimeConstraint | None:
 8032        if not self._match_text_seq("FOR", "SYSTEM_TIME"):
 8033            self._retreat(self._index - 1)
 8034            return None
 8035
 8036        id_vars = self._parse_wrapped_id_vars()
 8037        return self.expression(
 8038            exp.PeriodForSystemTimeConstraint(
 8039                this=seq_get(id_vars, 0), expression=seq_get(id_vars, 1)
 8040            )
 8041        )
 8042
 8043    def _parse_primary_key(
 8044        self,
 8045        wrapped_optional: bool = False,
 8046        in_props: bool = False,
 8047        named_primary_key: bool = False,
 8048    ) -> exp.PrimaryKeyColumnConstraint | exp.PrimaryKey:
 8049        desc = (
 8050            self._prev.token_type == TokenType.DESC
 8051            if self._match_set((TokenType.ASC, TokenType.DESC))
 8052            else None
 8053        )
 8054
 8055        this = None
 8056        if (
 8057            named_primary_key
 8058            and self._curr.text.upper() not in self.CONSTRAINT_PARSERS
 8059            and self._next
 8060            and self._next.token_type == TokenType.L_PAREN
 8061        ):
 8062            this = self._parse_id_var()
 8063
 8064        if not in_props and not self._match(TokenType.L_PAREN, advance=False):
 8065            return self.expression(
 8066                exp.PrimaryKeyColumnConstraint(
 8067                    desc=desc, options=self._parse_key_constraint_options()
 8068                )
 8069            )
 8070
 8071        expressions = self._parse_wrapped_csv(
 8072            self._parse_primary_key_part, optional=wrapped_optional
 8073        )
 8074
 8075        return self.expression(
 8076            exp.PrimaryKey(
 8077                this=this,
 8078                expressions=expressions,
 8079                include=self._parse_index_params(),
 8080                options=self._parse_key_constraint_options(),
 8081            )
 8082        )
 8083
 8084    def _parse_bracket_key_value(self, is_map: bool = False) -> exp.Expr | None:
 8085        return self._parse_slice(self._parse_alias(self._parse_disjunction(), explicit=True))
 8086
 8087    def _parse_odbc_datetime_literal(self) -> exp.Expr:
 8088        """
 8089        Parses a datetime column in ODBC format. We parse the column into the corresponding
 8090        types, for example `{d'yyyy-mm-dd'}` will be parsed as a `Date` column, exactly the
 8091        same as we did for `DATE('yyyy-mm-dd')`.
 8092
 8093        Reference:
 8094        https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/date-time-and-timestamp-literals
 8095        """
 8096        self._match(TokenType.VAR)
 8097        exp_class = self.ODBC_DATETIME_LITERALS[self._prev.text.lower()]
 8098        expression = self.expression(exp_class(this=self._parse_string()))
 8099        if not self._match(TokenType.R_BRACE):
 8100            self.raise_error("Expected }")
 8101        return expression
 8102
 8103    def _parse_bracket(self, this: exp.Expr | None = None) -> exp.Expr | None:
 8104        if not self._match_set(self.BRACKETS):
 8105            return this
 8106
 8107        if self.MAP_KEYS_ARE_ARBITRARY_EXPRESSIONS:
 8108            map_token = seq_get(self._tokens, self._index - 2)
 8109            parse_map = map_token is not None and map_token.text.upper() == "MAP"
 8110        else:
 8111            parse_map = False
 8112
 8113        bracket_kind = self._prev.token_type
 8114        if (
 8115            bracket_kind == TokenType.L_BRACE
 8116            and self._curr
 8117            and self._curr.token_type == TokenType.VAR
 8118            and self._curr.text.lower() in self.ODBC_DATETIME_LITERALS
 8119        ):
 8120            return self._parse_odbc_datetime_literal()
 8121
 8122        expressions = self._parse_csv(
 8123            lambda: self._parse_bracket_key_value(is_map=bracket_kind == TokenType.L_BRACE)
 8124        )
 8125
 8126        if bracket_kind == TokenType.L_BRACKET and not self._match(TokenType.R_BRACKET):
 8127            self.raise_error("Expected ]")
 8128        elif bracket_kind == TokenType.L_BRACE and not self._match(TokenType.R_BRACE):
 8129            self.raise_error("Expected }")
 8130
 8131        # https://duckdb.org/docs/sql/data_types/struct.html#creating-structs
 8132        if bracket_kind == TokenType.L_BRACE:
 8133            this = self.expression(
 8134                exp.Struct(
 8135                    expressions=self._kv_to_prop_eq(expressions=expressions, parse_map=parse_map)
 8136                )
 8137            )
 8138        elif not this:
 8139            this = build_array_constructor(
 8140                exp.Array, args=expressions, bracket_kind=bracket_kind, dialect=self.dialect
 8141            )
 8142        else:
 8143            constructor_type = self.ARRAY_CONSTRUCTORS.get(this.name.upper())
 8144            if constructor_type:
 8145                return build_array_constructor(
 8146                    constructor_type,
 8147                    args=expressions,
 8148                    bracket_kind=bracket_kind,
 8149                    dialect=self.dialect,
 8150                )
 8151
 8152            expressions = apply_index_offset(
 8153                this, expressions, -self.dialect.INDEX_OFFSET, dialect=self.dialect
 8154            )
 8155            this = self.expression(
 8156                exp.Bracket(this=this, expressions=expressions), comments=this.pop_comments()
 8157            )
 8158
 8159        self._add_comments(this)
 8160        return self._parse_bracket(this)
 8161
 8162    def _parse_slice(self, this: exp.Expr | None) -> exp.Expr | None:
 8163        if not self._match(TokenType.COLON):
 8164            return this
 8165
 8166        if self._match_pair(TokenType.DASH, TokenType.COLON, advance=False):
 8167            self._advance()
 8168            end: exp.Expr | None = -exp.Literal.number("1")
 8169        else:
 8170            end = self._parse_assignment()
 8171        step = self._parse_unary() if self._match(TokenType.COLON) else None
 8172        return self.expression(exp.Slice(this=this, expression=end, step=step))
 8173
 8174    def _parse_case(self) -> exp.Expr | None:
 8175        if self._match(TokenType.DOT, advance=False):
 8176            # Avoid raising on valid expressions like case.*, supported by, e.g., spark & snowflake
 8177            self._retreat(self._index - 1)
 8178            return None
 8179
 8180        ifs = []
 8181        default = None
 8182
 8183        comments = self._prev_comments
 8184        expression = self._parse_disjunction()
 8185
 8186        while self._match(TokenType.WHEN):
 8187            this = self._parse_disjunction()
 8188            self._match(TokenType.THEN)
 8189            then = self._parse_disjunction()
 8190            ifs.append(self.expression(exp.If(this=this, true=then)))
 8191
 8192        if self._match(TokenType.ELSE):
 8193            default = self._parse_disjunction()
 8194
 8195        if not self._match(TokenType.END):
 8196            if isinstance(default, exp.Interval) and default.this.sql().upper() == "END":
 8197                default = exp.column("interval")
 8198            else:
 8199                self.raise_error("Expected END after CASE", self._prev)
 8200
 8201        return self.expression(
 8202            exp.Case(this=expression, ifs=ifs, default=default), comments=comments
 8203        )
 8204
 8205    def _parse_if(self) -> exp.Expr | None:
 8206        if self._match(TokenType.L_PAREN):
 8207            args = self._parse_csv(
 8208                lambda: self._parse_alias(self._parse_assignment(), explicit=True)
 8209            )
 8210            this = self.validate_expression(exp.If.from_arg_list(args), args)
 8211            self._match_r_paren()
 8212        else:
 8213            index = self._index - 1
 8214
 8215            if self.NO_PAREN_IF_COMMANDS and index == 0:
 8216                return self._parse_as_command(self._prev)
 8217
 8218            condition = self._parse_disjunction()
 8219
 8220            if not condition or not self._match(TokenType.THEN):
 8221                self._retreat(index)
 8222                return None
 8223
 8224            true = self._parse_disjunction()
 8225            false = self._parse_disjunction() if self._match(TokenType.ELSE) else None
 8226            self._match(TokenType.END)
 8227            this = self.expression(exp.If(this=condition, true=true, false=false))
 8228
 8229        return this
 8230
 8231    def _parse_next_value_for(self) -> exp.Expr | None:
 8232        if not self._match_text_seq("VALUE", "FOR"):
 8233            self._retreat(self._index - 1)
 8234            return None
 8235
 8236        return self.expression(
 8237            exp.NextValueFor(
 8238                this=self._parse_column(),
 8239                order=self._match(TokenType.OVER) and self._parse_wrapped(self._parse_order),
 8240            )
 8241        )
 8242
 8243    def _parse_extract(self) -> exp.Extract | exp.Anonymous:
 8244        this = self._parse_function() or self._parse_var_or_string(upper=True)
 8245
 8246        if self._match(TokenType.FROM):
 8247            return self.expression(exp.Extract(this=this, expression=self._parse_bitwise()))
 8248
 8249        if not self._match(TokenType.COMMA):
 8250            self.raise_error("Expected FROM or comma after EXTRACT", self._prev)
 8251
 8252        return self.expression(exp.Extract(this=this, expression=self._parse_bitwise()))
 8253
 8254    def _parse_gap_fill(self) -> exp.GapFill:
 8255        self._match(TokenType.TABLE)
 8256        this = self._parse_table()
 8257
 8258        self._match(TokenType.COMMA)
 8259        args = [this, *self._parse_csv(self._parse_lambda)]
 8260
 8261        gap_fill = exp.GapFill.from_arg_list(args)
 8262        return self.validate_expression(gap_fill, args)
 8263
 8264    def _parse_char(self) -> exp.Chr:
 8265        return self.expression(
 8266            exp.Chr(
 8267                expressions=self._parse_csv(self._parse_assignment),
 8268                charset=self._match(TokenType.USING) and self._parse_charset_name(),
 8269            )
 8270        )
 8271
 8272    def _parse_charset_name(self) -> exp.Expr | None:
 8273        """
 8274        Parse a charset name after USING or CHARACTER SET. Dialects that need to preserve quoting
 8275        for specific name shapes override this.
 8276        """
 8277        return self._parse_var(
 8278            tokens={TokenType.BINARY, TokenType.IDENTIFIER},
 8279        )
 8280
 8281    def _parse_weight_string(self) -> exp.WeightString | exp.Anonymous:
 8282        name = self._tokens[self._index - 2].text
 8283        args = self._parse_csv(self._parse_assignment)
 8284
 8285        # MySQL also accepts an undocumented WEIGHT_STRING(str, n, n, n) form
 8286        if len(args) != 1:
 8287            return self.expression(exp.Anonymous(this=name, expressions=args))
 8288
 8289        return self.expression(
 8290            exp.WeightString(this=args[0], to=self._match(TokenType.ALIAS) and self._parse_types())
 8291        )
 8292
 8293    def _parse_cast(self, strict: bool, safe: bool | None = None) -> exp.Expr:
 8294        this = self._parse_assignment()
 8295
 8296        if not self._match(TokenType.ALIAS):
 8297            if self._match(TokenType.COMMA):
 8298                return self.expression(exp.CastToStrType(this=this, to=self._parse_string()))
 8299
 8300            self.raise_error("Expected AS after CAST")
 8301
 8302        fmt = None
 8303        to = self._parse_types(with_collation=True)
 8304
 8305        default = None
 8306        if self._match(TokenType.DEFAULT):
 8307            default = self._parse_bitwise()
 8308            self._match_text_seq("ON", "CONVERSION", "ERROR")
 8309
 8310        if self._match_set((TokenType.FORMAT, TokenType.COMMA)):
 8311            fmt_string = self._parse_wrapped(self._parse_string, optional=True)
 8312            fmt = self._parse_at_time_zone(fmt_string)
 8313
 8314            if not to:
 8315                to = exp.DType.UNKNOWN.into_expr()
 8316            if to.this in exp.DataType.TEMPORAL_TYPES:
 8317                this = self.expression(
 8318                    (exp.StrToDate if to.this == exp.DType.DATE else exp.StrToTime)(
 8319                        this=this,
 8320                        format=exp.Literal.string(
 8321                            format_time(
 8322                                fmt_string.this if fmt_string else "",
 8323                                self.dialect.FORMAT_MAPPING or self.dialect.TIME_MAPPING,
 8324                                self.dialect.FORMAT_TRIE or self.dialect.TIME_TRIE,
 8325                            )
 8326                        ),
 8327                        safe=safe,
 8328                    )
 8329                )
 8330
 8331                if isinstance(fmt, exp.AtTimeZone) and isinstance(this, exp.StrToTime):
 8332                    this.set("zone", fmt.args["zone"])
 8333                return this
 8334        elif not to:
 8335            self.raise_error("Expected TYPE after CAST")
 8336        elif isinstance(to, exp.Identifier):
 8337            to = exp.DataType.from_str(to.name, dialect=self.dialect, udt=True)
 8338        else:
 8339            to = self._parse_char_charset(to)
 8340
 8341        return self.build_cast(
 8342            strict=strict,
 8343            this=this,
 8344            to=to,
 8345            format=fmt,
 8346            safe=safe,
 8347            action=self._parse_var_from_options(self.CAST_ACTIONS, raise_unmatched=False),
 8348            default=default,
 8349        )
 8350
 8351    def _parse_string_agg(self) -> exp.GroupConcat:
 8352        if self._match(TokenType.DISTINCT):
 8353            args: list[exp.Expr | None] = [
 8354                self.expression(exp.Distinct(expressions=[self._parse_disjunction()]))
 8355            ]
 8356            if self._match(TokenType.COMMA):
 8357                args.extend(self._parse_csv(self._parse_disjunction))
 8358        else:
 8359            args = self._parse_csv(self._parse_disjunction)  # type: ignore
 8360
 8361        if self._match_text_seq("ON", "OVERFLOW"):
 8362            # trino: LISTAGG(expression [, separator] [ON OVERFLOW overflow_behavior])
 8363            if self._match_text_seq("ERROR"):
 8364                on_overflow: exp.Expr | None = exp.var("ERROR")
 8365            else:
 8366                self._match_text_seq("TRUNCATE")
 8367                on_overflow = self.expression(
 8368                    exp.OverflowTruncateBehavior(
 8369                        this=self._parse_string(),
 8370                        with_count=(
 8371                            self._match_text_seq("WITH", "COUNT")
 8372                            or not self._match_text_seq("WITHOUT", "COUNT")
 8373                        ),
 8374                    )
 8375                )
 8376        else:
 8377            on_overflow = None
 8378
 8379        index = self._index
 8380        if not self._match(TokenType.R_PAREN) and args:
 8381            # postgres: STRING_AGG([DISTINCT] expression, separator [ORDER BY expression1 {ASC | DESC} [, ...]])
 8382            # bigquery: STRING_AGG([DISTINCT] expression [, separator] [ORDER BY key [{ASC | DESC}] [, ... ]] [LIMIT n])
 8383            # The order is parsed through `this` as a canonicalization for WITHIN GROUPs
 8384            args[0] = self._parse_limit(this=self._parse_order(this=args[0]))
 8385            return self.expression(exp.GroupConcat(this=args[0], separator=seq_get(args, 1)))
 8386
 8387        # Checks if we can parse an order clause: WITHIN GROUP (ORDER BY <order_by_expression_list> [ASC | DESC]).
 8388        # This is done "manually", instead of letting _parse_window parse it into an exp.WithinGroup node, so that
 8389        # the STRING_AGG call is parsed like in MySQL / SQLite and can thus be transpiled more easily to them.
 8390        if not self._match_text_seq("WITHIN", "GROUP"):
 8391            self._retreat(index)
 8392            return self.validate_expression(exp.GroupConcat.from_arg_list(args), args)
 8393
 8394        # The corresponding match_r_paren will be called in parse_function (caller)
 8395        self._match_l_paren()
 8396
 8397        return self.expression(
 8398            exp.GroupConcat(
 8399                this=self._parse_order(this=seq_get(args, 0)),
 8400                separator=seq_get(args, 1),
 8401                on_overflow=on_overflow,
 8402            )
 8403        )
 8404
 8405    def _parse_convert(self, strict: bool, safe: bool | None = None) -> exp.Expr | None:
 8406        this = self._parse_bitwise()
 8407
 8408        if self._match(TokenType.USING):
 8409            to: exp.Expr | None = exp.DType.CHARACTER_SET.into_expr(kind=self._parse_charset_name())
 8410        elif self._match(TokenType.COMMA):
 8411            to = self._parse_char_charset(self._parse_types())
 8412        else:
 8413            to = None
 8414
 8415        return self.build_cast(strict=strict, this=this, to=to, safe=safe)
 8416
 8417    def _parse_char_charset(self, to: exp.Expr | None) -> exp.Expr | None:
 8418        # MySQL: CHAR[(N)] CHARACTER SET charset_name, the length is kept
 8419        if (
 8420            isinstance(to, exp.DataType)
 8421            and to.this == exp.DType.CHAR
 8422            and (self._match(TokenType.CHARACTER_SET) or self._match_text_seq("CHARACTER", "SET"))
 8423        ):
 8424            to.set("this", exp.DType.CHARACTER_SET)
 8425            to.set("kind", self._parse_var_or_string())
 8426        return to
 8427
 8428    def _parse_xml_element(self) -> exp.XMLElement:
 8429        if self._match_text_seq("EVALNAME"):
 8430            evalname = True
 8431            this = self._parse_bitwise()
 8432        else:
 8433            evalname = None
 8434            self._match_text_seq("NAME")
 8435            this = self._parse_id_var()
 8436
 8437        return self.expression(
 8438            exp.XMLElement(
 8439                this=this,
 8440                expressions=self._match(TokenType.COMMA) and self._parse_csv(self._parse_bitwise),
 8441                evalname=evalname,
 8442            )
 8443        )
 8444
 8445    def _parse_xml_table(self) -> exp.XMLTable:
 8446        namespaces = None
 8447        passing = None
 8448        columns = None
 8449
 8450        if self._match_text_seq("XMLNAMESPACES", "("):
 8451            namespaces = self._parse_xml_namespace()
 8452            self._match_text_seq(")", ",")
 8453
 8454        this = self._parse_string()
 8455
 8456        if self._match_text_seq("PASSING"):
 8457            # The BY VALUE keywords are optional and are provided for semantic clarity
 8458            self._match_text_seq("BY", "VALUE")
 8459            passing = self._parse_csv(self._parse_column)
 8460
 8461        by_ref = self._match_text_seq("RETURNING", "SEQUENCE", "BY", "REF")
 8462
 8463        if self._match_text_seq("COLUMNS"):
 8464            columns = self._parse_csv(self._parse_field_def)
 8465
 8466        return self.expression(
 8467            exp.XMLTable(
 8468                this=this, namespaces=namespaces, passing=passing, columns=columns, by_ref=by_ref
 8469            )
 8470        )
 8471
 8472    def _parse_xml_namespace(self) -> list[exp.XMLNamespace]:
 8473        namespaces = []
 8474
 8475        while True:
 8476            if self._match(TokenType.DEFAULT):
 8477                uri = self._parse_string()
 8478            else:
 8479                uri = self._parse_alias(self._parse_string())
 8480            namespaces.append(self.expression(exp.XMLNamespace(this=uri)))
 8481            if not self._match(TokenType.COMMA):
 8482                break
 8483
 8484        return namespaces
 8485
 8486    def _parse_decode(self) -> exp.Decode | exp.DecodeCase | None:
 8487        args = self._parse_csv(self._parse_disjunction)
 8488
 8489        if len(args) < 3:
 8490            return self.expression(exp.Decode(this=seq_get(args, 0), charset=seq_get(args, 1)))
 8491
 8492        return self.expression(exp.DecodeCase(expressions=args))
 8493
 8494    def _parse_json_key_value(self) -> exp.JSONKeyValue | None:
 8495        self._match_text_seq("KEY")
 8496        key = self._parse_column()
 8497        self._match_set(self.JSON_KEY_VALUE_SEPARATOR_TOKENS)
 8498        self._match_text_seq("VALUE")
 8499        value = self._parse_bitwise()
 8500
 8501        if not key and not value:
 8502            return None
 8503        return self.expression(exp.JSONKeyValue(this=key, expression=value))
 8504
 8505    def _parse_format_json(self, this: exp.Expr | None) -> exp.Expr | None:
 8506        if not this or not self._match_text_seq("FORMAT", "JSON"):
 8507            return this
 8508
 8509        return self.expression(exp.FormatJson(this=this))
 8510
 8511    def _parse_on_condition(self) -> exp.OnCondition | None:
 8512        # MySQL uses "X ON EMPTY Y ON ERROR" (e.g. JSON_VALUE) while Oracle uses the opposite (e.g. JSON_EXISTS)
 8513        if self.dialect.ON_CONDITION_EMPTY_BEFORE_ERROR:
 8514            empty = self._parse_on_handling("EMPTY", *self.ON_CONDITION_TOKENS)
 8515            error = self._parse_on_handling("ERROR", *self.ON_CONDITION_TOKENS)
 8516        else:
 8517            error = self._parse_on_handling("ERROR", *self.ON_CONDITION_TOKENS)
 8518            empty = self._parse_on_handling("EMPTY", *self.ON_CONDITION_TOKENS)
 8519
 8520        null = self._parse_on_handling("NULL", *self.ON_CONDITION_TOKENS)
 8521
 8522        if not empty and not error and not null:
 8523            return None
 8524
 8525        return self.expression(exp.OnCondition(empty=empty, error=error, null=null))
 8526
 8527    def _parse_on_handling(self, on: str, *values: str) -> str | None | exp.Expr | None:
 8528        # Parses the "X ON Y" or "DEFAULT <expr> ON Y syntax, e.g. NULL ON NULL (Oracle, T-SQL, MySQL)
 8529        for value in values:
 8530            if self._match_text_seq(value, "ON", on):
 8531                return f"{value} ON {on}"
 8532
 8533        index = self._index
 8534        if self._match(TokenType.DEFAULT):
 8535            default_value = self._parse_bitwise()
 8536            if self._match_text_seq("ON", on):
 8537                return default_value
 8538
 8539            self._retreat(index)
 8540
 8541        return None
 8542
 8543    @t.overload
 8544    def _parse_json_object(self, agg: t.Literal[False]) -> exp.JSONObject: ...
 8545
 8546    @t.overload
 8547    def _parse_json_object(self, agg: t.Literal[True]) -> exp.JSONObjectAgg: ...
 8548
 8549    def _parse_json_object(self, agg=False):
 8550        star = self._parse_star()
 8551        expressions = (
 8552            [star]
 8553            if star
 8554            else self._parse_csv(lambda: self._parse_format_json(self._parse_json_key_value()))
 8555        )
 8556        null_handling = self._parse_on_handling("NULL", "NULL", "ABSENT")
 8557
 8558        unique_keys = None
 8559        if self._match_text_seq("WITH", "UNIQUE"):
 8560            unique_keys = True
 8561        elif self._match_text_seq("WITHOUT", "UNIQUE"):
 8562            unique_keys = False
 8563
 8564        self._match_text_seq("KEYS")
 8565
 8566        return_type = self._match_text_seq("RETURNING") and self._parse_format_json(
 8567            self._parse_type()
 8568        )
 8569        encoding = self._match_text_seq("ENCODING") and self._parse_var()
 8570
 8571        return self.expression(
 8572            (exp.JSONObjectAgg if agg else exp.JSONObject)(
 8573                expressions=expressions,
 8574                null_handling=null_handling,
 8575                unique_keys=unique_keys,
 8576                return_type=return_type,
 8577                encoding=encoding,
 8578            )
 8579        )
 8580
 8581    # Note: this is currently incomplete; it only implements the "JSON_value_column" part
 8582    def _parse_json_column_def(self) -> exp.JSONColumnDef:
 8583        if not self._match_text_seq("NESTED"):
 8584            this = self._parse_id_var()
 8585            ordinality = self._match_pair(TokenType.FOR, TokenType.ORDINALITY)
 8586            kind = self._parse_types(allow_identifiers=False)
 8587            nested = None
 8588        else:
 8589            this = None
 8590            ordinality = None
 8591            kind = None
 8592            nested = True
 8593
 8594        format_json = self._match_text_seq("FORMAT", "JSON")
 8595        path = self._match_text_seq("PATH") and self._parse_string()
 8596        nested_schema = nested and self._parse_json_schema()
 8597
 8598        return self.expression(
 8599            exp.JSONColumnDef(
 8600                this=this,
 8601                kind=kind,
 8602                path=path,
 8603                nested_schema=nested_schema,
 8604                ordinality=ordinality,
 8605                format_json=format_json,
 8606            )
 8607        )
 8608
 8609    def _parse_json_schema(self) -> exp.JSONSchema:
 8610        self._match_text_seq("COLUMNS")
 8611        return self.expression(
 8612            exp.JSONSchema(
 8613                expressions=self._parse_wrapped_csv(self._parse_json_column_def, optional=True)
 8614            )
 8615        )
 8616
 8617    def _parse_json_table(self) -> exp.JSONTable:
 8618        this = self._parse_format_json(self._parse_bitwise())
 8619        path = self._match(TokenType.COMMA) and self._parse_string()
 8620        error_handling = self._parse_on_handling("ERROR", "ERROR", "NULL")
 8621        empty_handling = self._parse_on_handling("EMPTY", "ERROR", "NULL")
 8622        schema = self._parse_json_schema()
 8623
 8624        return exp.JSONTable(
 8625            this=this,
 8626            schema=schema,
 8627            path=path,
 8628            error_handling=error_handling,
 8629            empty_handling=empty_handling,
 8630        )
 8631
 8632    def _parse_match_against(self) -> exp.MatchAgainst:
 8633        if self._match_text_seq("TABLE"):
 8634            # parse SingleStore MATCH(TABLE ...) syntax
 8635            # https://docs.singlestore.com/cloud/reference/sql-reference/full-text-search-functions/match/
 8636            expressions = []
 8637            table = self._parse_table()
 8638            if table:
 8639                expressions = [table]
 8640        else:
 8641            expressions = self._parse_csv(self._parse_column)
 8642
 8643        self._match_text_seq(")", "AGAINST", "(")
 8644
 8645        this = self._parse_string()
 8646
 8647        if self._match_text_seq("IN", "NATURAL", "LANGUAGE", "MODE"):
 8648            modifier = "IN NATURAL LANGUAGE MODE"
 8649            if self._match_text_seq("WITH", "QUERY", "EXPANSION"):
 8650                modifier = f"{modifier} WITH QUERY EXPANSION"
 8651        elif self._match_text_seq("IN", "BOOLEAN", "MODE"):
 8652            modifier = "IN BOOLEAN MODE"
 8653        elif self._match_text_seq("WITH", "QUERY", "EXPANSION"):
 8654            modifier = "WITH QUERY EXPANSION"
 8655        else:
 8656            modifier = None
 8657
 8658        return self.expression(
 8659            exp.MatchAgainst(this=this, expressions=expressions, modifier=modifier)
 8660        )
 8661
 8662    # https://learn.microsoft.com/en-us/sql/t-sql/functions/openjson-transact-sql?view=sql-server-ver16
 8663    def _parse_open_json(self) -> exp.OpenJSON:
 8664        this = self._parse_bitwise()
 8665        path = self._match(TokenType.COMMA) and self._parse_string()
 8666
 8667        def _parse_open_json_column_def() -> exp.OpenJSONColumnDef:
 8668            this = self._parse_field(any_token=True)
 8669            kind = self._parse_types()
 8670            path = self._parse_string()
 8671            as_json = self._match_pair(TokenType.ALIAS, TokenType.JSON)
 8672
 8673            return self.expression(
 8674                exp.OpenJSONColumnDef(this=this, kind=kind, path=path, as_json=as_json)
 8675            )
 8676
 8677        expressions = None
 8678        if self._match_pair(TokenType.R_PAREN, TokenType.WITH):
 8679            self._match_l_paren()
 8680            expressions = self._parse_csv(_parse_open_json_column_def)
 8681
 8682        return self.expression(exp.OpenJSON(this=this, path=path, expressions=expressions))
 8683
 8684    def _parse_position(self, haystack_first: bool = False) -> exp.StrPosition:
 8685        args = self._parse_csv(self._parse_bitwise)
 8686
 8687        if self._match(TokenType.IN):
 8688            return self.expression(
 8689                exp.StrPosition(this=self._parse_bitwise(), substr=seq_get(args, 0))
 8690            )
 8691
 8692        if haystack_first:
 8693            haystack = seq_get(args, 0)
 8694            needle = seq_get(args, 1)
 8695        else:
 8696            haystack = seq_get(args, 1)
 8697            needle = seq_get(args, 0)
 8698
 8699        return self.expression(
 8700            exp.StrPosition(this=haystack, substr=needle, position=seq_get(args, 2))
 8701        )
 8702
 8703    def _parse_join_hint(self, func_name: str) -> exp.JoinHint:
 8704        args = self._parse_csv(self._parse_table)
 8705        return exp.JoinHint(this=func_name.upper(), expressions=args)
 8706
 8707    def _parse_substring(self) -> exp.Substring:
 8708        # Postgres supports the form: substring(string [from int] [for int])
 8709        # (despite being undocumented, the reverse order also works)
 8710        # https://www.postgresql.org/docs/9.1/functions-string.html @ Table 9-6
 8711
 8712        args = t.cast(list[t.Optional[exp.Expr]], self._parse_csv(self._parse_bitwise))
 8713
 8714        start, length = None, None
 8715
 8716        while self._curr:
 8717            if self._match(TokenType.FROM):
 8718                start = self._parse_bitwise()
 8719            elif self._match(TokenType.FOR):
 8720                if not start:
 8721                    start = exp.Literal.number(1)
 8722                length = self._parse_bitwise()
 8723            else:
 8724                break
 8725
 8726        if start:
 8727            args.append(start)
 8728        if length:
 8729            args.append(length)
 8730
 8731        return self.validate_expression(exp.Substring.from_arg_list(args), args)
 8732
 8733    def _parse_trim(self) -> exp.Trim:
 8734        # https://www.w3resource.com/sql/character-functions/trim.php
 8735        # https://docs.oracle.com/javadb/10.8.3.0/ref/rreftrimfunc.html
 8736
 8737        position = None
 8738        collation = None
 8739        expression = None
 8740
 8741        if self._match_texts(self.TRIM_TYPES):
 8742            position = self._prev.text.upper()
 8743
 8744        this = self._parse_bitwise()
 8745        if self._match_set((TokenType.FROM, TokenType.COMMA)):
 8746            invert_order = self._prev.token_type == TokenType.FROM or self.TRIM_PATTERN_FIRST
 8747            expression = self._parse_bitwise()
 8748
 8749            if invert_order:
 8750                this, expression = expression, this
 8751
 8752        if self._match(TokenType.COLLATE):
 8753            collation = self._parse_bitwise()
 8754
 8755        return self.expression(
 8756            exp.Trim(this=this, position=position, expression=expression, collation=collation)
 8757        )
 8758
 8759    def _parse_window_clause(self) -> list[exp.Expr] | None:
 8760        return self._parse_csv(self._parse_named_window) if self._match(TokenType.WINDOW) else None
 8761
 8762    def _parse_named_window(self) -> exp.Expr | None:
 8763        return self._parse_window(self._parse_id_var(), alias=True)
 8764
 8765    def _parse_respect_or_ignore_nulls(self, this: exp.Expr | None) -> exp.Expr | None:
 8766        if self._curr.token_type == TokenType.VAR:
 8767            if self._match_text_seq("IGNORE", "NULLS"):
 8768                return self.expression(exp.IgnoreNulls(this=this))
 8769            if self._match_text_seq("RESPECT", "NULLS"):
 8770                return self.expression(exp.RespectNulls(this=this))
 8771        return this
 8772
 8773    def _parse_having_max(self, this: exp.Expr | None) -> exp.Expr | None:
 8774        if self._match(TokenType.HAVING):
 8775            self._match_texts(("MAX", "MIN"))
 8776            max = self._prev.text.upper() != "MIN"
 8777            return self.expression(
 8778                exp.HavingMax(this=this, expression=self._parse_column(), max=max)
 8779            )
 8780
 8781        return this
 8782
 8783    def _parse_window(self, this: exp.Expr | None, alias: bool = False) -> exp.Expr | None:
 8784        func = this
 8785        comments = func.comments if isinstance(func, exp.Expr) else None
 8786
 8787        # https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/img_text/nth_value.html
 8788        if self.SUPPORTS_NTH_VALUE_FROM_MODIFIER and isinstance(this, exp.NthValue):
 8789            if self._match_text_seq("FROM", "FIRST"):
 8790                this.set("from_first", True)
 8791            elif self._match_text_seq("FROM", "LAST"):
 8792                this.set("from_first", False)
 8793
 8794        # T-SQL allows the OVER (...) syntax after WITHIN GROUP.
 8795        # https://learn.microsoft.com/en-us/sql/t-sql/functions/percentile-disc-transact-sql?view=sql-server-ver16
 8796        if self._match_text_seq("WITHIN", "GROUP"):
 8797            order = self._parse_wrapped(self._parse_order)
 8798            this = self.expression(exp.WithinGroup(this=this, expression=order))
 8799
 8800        if self._match_pair(TokenType.FILTER, TokenType.L_PAREN):
 8801            self._match(TokenType.WHERE)
 8802            this = self.expression(
 8803                exp.Filter(this=this, expression=self._parse_where(skip_where_token=True))
 8804            )
 8805            self._match_r_paren()
 8806
 8807        # SQL spec defines an optional [ { IGNORE | RESPECT } NULLS ] OVER
 8808        # Some dialects choose to implement and some do not.
 8809        # https://dev.mysql.com/doc/refman/8.0/en/window-function-descriptions.html
 8810
 8811        # There is some code above in _parse_lambda that handles
 8812        #   SELECT FIRST_VALUE(TABLE.COLUMN IGNORE|RESPECT NULLS) OVER ...
 8813
 8814        # The below changes handle
 8815        #   SELECT FIRST_VALUE(TABLE.COLUMN) IGNORE|RESPECT NULLS OVER ...
 8816
 8817        # Oracle allows both formats
 8818        #   (https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/img_text/first_value.html)
 8819        #   and Snowflake chose to do the same for familiarity
 8820        #   https://docs.snowflake.com/en/sql-reference/functions/first_value.html#usage-notes
 8821        if isinstance(this, exp.AggFunc):
 8822            ignore_respect = find_in_scope(this, exp.IgnoreNulls, exp.RespectNulls)
 8823
 8824            if ignore_respect and ignore_respect is not this:
 8825                ignore_respect.replace(ignore_respect.this)
 8826                this = self.expression(ignore_respect.__class__(this=this))
 8827
 8828        this = self._parse_respect_or_ignore_nulls(this)
 8829
 8830        # bigquery select from window x AS (partition by ...)
 8831        if alias:
 8832            over = None
 8833            self._match(TokenType.ALIAS)
 8834        elif not self._match_set(self.WINDOW_BEFORE_PAREN_TOKENS):
 8835            return this
 8836        else:
 8837            over = self._prev.text.upper()
 8838
 8839        if comments and isinstance(func, exp.Expr):
 8840            func.pop_comments()
 8841
 8842        if not self._match(TokenType.L_PAREN):
 8843            return self.expression(
 8844                exp.Window(this=this, alias=self._parse_id_var(False), over=over), comments=comments
 8845            )
 8846
 8847        window_alias = self._parse_id_var(any_token=False, tokens=self.WINDOW_ALIAS_TOKENS)
 8848
 8849        first: bool | None = True if self._match(TokenType.FIRST) else None
 8850        if self._match_text_seq("LAST"):
 8851            first = False
 8852
 8853        partition, order = self._parse_partition_and_order()
 8854        kind = (
 8855            self._match_set((TokenType.ROWS, TokenType.RANGE)) or self._match_text_seq("GROUPS")
 8856        ) and self._prev.text
 8857
 8858        if kind:
 8859            self._match(TokenType.BETWEEN)
 8860            start = self._parse_window_spec()
 8861
 8862            end = self._parse_window_spec() if self._match(TokenType.AND) else {}
 8863            exclude = (
 8864                self._parse_var_from_options(self.WINDOW_EXCLUDE_OPTIONS)
 8865                if self._match_text_seq("EXCLUDE")
 8866                else None
 8867            )
 8868
 8869            spec = self.expression(
 8870                exp.WindowSpec(
 8871                    kind=kind,
 8872                    start=start["value"],
 8873                    start_side=start["side"],
 8874                    end=end.get("value"),
 8875                    end_side=end.get("side"),
 8876                    exclude=exclude,
 8877                )
 8878            )
 8879        else:
 8880            spec = None
 8881
 8882        self._match_r_paren()
 8883
 8884        window = self.expression(
 8885            exp.Window(
 8886                this=this,
 8887                partition_by=partition,
 8888                order=order,
 8889                spec=spec,
 8890                alias=window_alias,
 8891                over=over,
 8892                first=first,
 8893            ),
 8894            comments=comments,
 8895        )
 8896
 8897        # This covers Oracle's FIRST/LAST syntax: aggregate KEEP (...) OVER (...)
 8898        if self._match_set(self.WINDOW_BEFORE_PAREN_TOKENS, advance=False):
 8899            return self._parse_window(window, alias=alias)
 8900
 8901        return window
 8902
 8903    def _parse_partition_and_order(
 8904        self,
 8905    ) -> tuple[list[exp.Expr], exp.Expr | None]:
 8906        return self._parse_partition_by(), self._parse_order()
 8907
 8908    def _parse_window_spec(self) -> dict[str, str | exp.Expr | None]:
 8909        self._match(TokenType.BETWEEN)
 8910
 8911        return {
 8912            "value": (
 8913                (self._match_text_seq("UNBOUNDED") and "UNBOUNDED")
 8914                or (self._match_text_seq("CURRENT", "ROW") and "CURRENT ROW")
 8915                or self._parse_bitwise()
 8916            ),
 8917            "side": self._prev.text if self._match_texts(self.WINDOW_SIDES) else None,
 8918        }
 8919
 8920    def _parse_alias(self, this: exp.Expr | None, explicit: bool = False) -> exp.Expr | None:
 8921        # In some dialects, LIMIT and OFFSET can act as both identifiers and keywords (clauses)
 8922        # so this section tries to parse the clause version and if it fails, it treats the token
 8923        # as an identifier (alias)
 8924        if self._can_parse_limit_or_offset():
 8925            return this
 8926
 8927        # WINDOW is in ID_VAR_TOKENS, so it can be consumed as an implicit alias. Detect the
 8928        # named-window clause shape (`WINDOW <ident> AS (...)`) and avoid swallowing it.
 8929        if self._can_parse_named_window():
 8930            return this
 8931
 8932        any_token = self._match(TokenType.ALIAS)
 8933        comments = self._prev_comments
 8934
 8935        if explicit and not any_token:
 8936            return this
 8937
 8938        if self._match(TokenType.L_PAREN):
 8939            aliases = self.expression(
 8940                exp.Aliases(
 8941                    this=this, expressions=self._parse_csv(lambda: self._parse_id_var(any_token))
 8942                ),
 8943                comments=comments,
 8944            )
 8945            self._match_r_paren(aliases)
 8946            return aliases
 8947
 8948        alias = self._parse_id_var(any_token, tokens=self.ALIAS_TOKENS) or (
 8949            self.STRING_ALIASES and self._parse_string_as_identifier()
 8950        )
 8951
 8952        if alias:
 8953            comments.extend(alias.pop_comments())
 8954            this = self.expression(exp.Alias(this=this, alias=alias), comments=comments)
 8955            column = this.this
 8956
 8957            # Moves the comment next to the alias in `expr /* comment */ AS alias`
 8958            if not this.comments and column and column.comments:
 8959                this.comments = column.pop_comments()
 8960
 8961        return this
 8962
 8963    def _parse_id_var(
 8964        self,
 8965        any_token: bool = True,
 8966        tokens: t.Collection[TokenType] | None = None,
 8967    ) -> exp.Expr | None:
 8968        expression = self._parse_identifier()
 8969        if not expression and (
 8970            (any_token and self._advance_any()) or self._match_set(tokens or self.ID_VAR_TOKENS)
 8971        ):
 8972            quoted = self._prev.token_type == TokenType.STRING
 8973            expression = self._identifier_expression(quoted=quoted)
 8974
 8975        return expression
 8976
 8977    def _parse_string(self) -> exp.Expr | None:
 8978        if self._match_set(self.STRING_PARSERS):
 8979            return self.STRING_PARSERS[self._prev.token_type](self, self._prev)
 8980        return self._parse_placeholder()
 8981
 8982    def _parse_string_as_identifier(self) -> exp.Identifier | None:
 8983        if not self._match(TokenType.STRING):
 8984            return None
 8985        output = exp.to_identifier(self._prev.text, quoted=True)
 8986        output.update_positions(self._prev)
 8987        return output
 8988
 8989    def _parse_number(self) -> exp.Expr | None:
 8990        if self._match_set(self.NUMERIC_PARSERS):
 8991            return self.NUMERIC_PARSERS[self._prev.token_type](self, self._prev)
 8992