Edit on GitHub

sqlmesh.core.dialect

   1from __future__ import annotations
   2
   3import functools
   4import logging
   5import re
   6import sys
   7import typing as t
   8from contextlib import contextmanager
   9from difflib import unified_diff
  10from enum import Enum, auto
  11from functools import lru_cache
  12
  13from sqlglot import Dialect, Generator, ParseError, Parser, Tokenizer, TokenType, exp
  14from sqlglot.dialects.dialect import DialectType
  15from sqlglot.dialects import DuckDB, Snowflake, TSQL
  16import sqlglot.dialects.athena as athena
  17import sqlglot.generators.athena as athena_generators
  18from sqlglot.parsers.athena import AthenaTrinoParser
  19from sqlglot.helper import seq_get
  20from sqlglot.optimizer.normalize_identifiers import normalize_identifiers
  21from sqlglot.optimizer.qualify_columns import quote_identifiers
  22from sqlglot.optimizer.qualify_tables import qualify_tables
  23from sqlglot.optimizer.scope import traverse_scope
  24from sqlglot.schema import MappingSchema
  25from sqlglot.tokens import Token
  26
  27from sqlmesh.core.constants import LIQUID_CLUSTERING_KEYWORDS, MAX_MODEL_DEFINITION_SIZE
  28from sqlmesh.utils import get_source_columns_to_types
  29from sqlmesh.utils.errors import SQLMeshError, ConfigError
  30from sqlmesh.utils.pandas import columns_to_types_from_df
  31
  32if t.TYPE_CHECKING:
  33    import pandas as pd
  34
  35    from sqlglot._typing import E
  36
  37
  38SQLMESH_MACRO_PREFIX = "@"
  39
  40TABLES_META = "sqlmesh.tables"
  41
  42logger = logging.getLogger(__name__)
  43
  44
  45class Model(exp.Expression):
  46    arg_types = {"expressions": True}
  47
  48
  49class Audit(exp.Expression):
  50    arg_types = {"expressions": True}
  51
  52
  53class Metric(exp.Expression):
  54    arg_types = {"expressions": True}
  55
  56
  57class Jinja(exp.Expression, exp.Func):
  58    arg_types = {"this": True}
  59
  60
  61class JinjaQuery(Jinja):
  62    pass
  63
  64
  65class JinjaStatement(Jinja):
  66    pass
  67
  68
  69class VirtualUpdateStatement(exp.Expression):
  70    arg_types = {"expressions": True}
  71
  72
  73class ModelKind(exp.Expression):
  74    arg_types = {"this": True, "expressions": False}
  75
  76
  77class MacroVar(exp.Var):
  78    pass
  79
  80
  81class MacroFunc(exp.Expression, exp.Func):
  82    @property
  83    def name(self) -> str:
  84        return self.this.name
  85
  86
  87class MacroDef(MacroFunc):
  88    arg_types = {"this": True, "expression": True}
  89
  90
  91class MacroSQL(MacroFunc):
  92    arg_types = {"this": True, "into": False}
  93
  94
  95class MacroStrReplace(MacroFunc):
  96    pass
  97
  98
  99class PythonCode(exp.Expression):
 100    arg_types = {"expressions": True}
 101
 102
 103class DColonCast(exp.Cast):
 104    pass
 105
 106
 107class MetricAgg(exp.Expression, exp.AggFunc):
 108    """Used for computing metrics."""
 109
 110    arg_types = {"this": True}
 111
 112    @property
 113    def output_name(self) -> str:
 114        return self.this.name
 115
 116
 117class StagedFilePath(exp.Expression):
 118    """Represents paths to "staged files" in Snowflake."""
 119
 120    arg_types = exp.Table.arg_types.copy()
 121
 122
 123def _parse_statement(self: Parser) -> t.Optional[exp.Expr]:
 124    if self._curr is None:
 125        return None
 126
 127    parser = PARSERS.get(self._curr.text.upper())
 128    error_msg = None
 129
 130    if parser:
 131        # Capture any available description in the form of a comment
 132        comments = self._curr.comments
 133
 134        index = self._index
 135        try:
 136            self._advance()
 137            meta = self._parse_wrapped(lambda: t.cast(t.Callable, parser)(self))
 138        except ParseError as parse_error:
 139            error_msg = parse_error.args[0]
 140            self._retreat(index)
 141
 142        # Only return the DDL expression if we actually managed to parse one. This is
 143        # done in order to allow parsing standalone identifiers / function calls like
 144        # "metric", or "model(1, 2, 3)", which collide with SQLMesh's DDL syntax.
 145        if self._index != index:
 146            meta.comments = comments
 147            return meta
 148
 149    try:
 150        return self.__parse_statement()  # type: ignore
 151    except ParseError:
 152        if error_msg:
 153            raise ParseError(error_msg)
 154        raise
 155
 156
 157def _parse_lambda(self: Parser, alias: bool = False) -> t.Optional[exp.Expr]:
 158    node = self.__parse_lambda(alias=alias)  # type: ignore
 159    if isinstance(node, exp.Lambda):
 160        node.set("this", self._parse_alias(node.this))
 161    return node
 162
 163
 164def _parse_id_var(
 165    self: Parser,
 166    any_token: bool = True,
 167    tokens: t.Optional[t.Collection[TokenType]] = None,
 168) -> t.Optional[exp.Expr]:
 169    if self._prev and self._prev.text == SQLMESH_MACRO_PREFIX and self._match(TokenType.L_BRACE):
 170        identifier = self.__parse_id_var(any_token=any_token, tokens=tokens)  # type: ignore
 171        if not self._match(TokenType.R_BRACE):
 172            self.raise_error("Expecting }")
 173        identifier.args["this"] = f"@{{{identifier.name}}}"
 174    else:
 175        identifier = self.__parse_id_var(any_token=any_token, tokens=tokens)  # type: ignore
 176
 177    while (
 178        identifier
 179        and not identifier.args.get("quoted")
 180        and self._is_connected()
 181        and (
 182            self._match_texts(("{", SQLMESH_MACRO_PREFIX))
 183            or self._curr.token_type not in self.RESERVED_TOKENS
 184        )
 185    ):
 186        this = identifier.name
 187        brace = False
 188
 189        if self._prev.text == "{":
 190            this += "{"
 191            brace = True
 192        else:
 193            if self._prev.text == SQLMESH_MACRO_PREFIX:
 194                this += "@"
 195            if self._match(TokenType.L_BRACE):
 196                this += "{"
 197                brace = True
 198
 199        next_id = self._parse_id_var(any_token=False)
 200
 201        if next_id:
 202            this += next_id.name
 203        else:
 204            return identifier
 205
 206        if brace:
 207            if self._match(TokenType.R_BRACE):
 208                this += "}"
 209            else:
 210                self.raise_error("Expecting }")
 211
 212        identifier = self.expression(exp.Identifier(this=this, quoted=identifier.quoted))
 213
 214    return identifier
 215
 216
 217def _parse_macro(self: Parser, keyword_macro: str = "") -> t.Optional[exp.Expr]:
 218    if self._prev.text != SQLMESH_MACRO_PREFIX:
 219        return self._parse_parameter()
 220
 221    comments = self._prev.comments
 222    index = self._index
 223    field = self._parse_primary() or self._parse_function(functions={}) or self._parse_id_var()
 224
 225    def _build_macro(field: t.Optional[exp.Expr]) -> t.Optional[exp.Expr]:
 226        if isinstance(field, exp.Func):
 227            macro_name = field.name.upper()
 228            if macro_name != keyword_macro and macro_name in KEYWORD_MACROS:
 229                self._retreat(index)
 230                return None
 231
 232            if isinstance(field, exp.Anonymous):
 233                if macro_name == "DEF":
 234                    return self.expression(
 235                        MacroDef(
 236                            this=field.expressions[0],
 237                            expression=field.expressions[1],
 238                        ),
 239                        comments=comments,
 240                    )
 241                if macro_name == "SQL":
 242                    into = field.expressions[1].this.lower() if len(field.expressions) > 1 else None
 243                    return self.expression(
 244                        MacroSQL(this=field.expressions[0], into=into), comments=comments
 245                    )
 246            else:
 247                field = self.expression(
 248                    exp.Anonymous(
 249                        this=field.sql_name(),
 250                        expressions=list(field.args.values()),
 251                    ),
 252                    comments=comments,
 253                )
 254
 255            return self.expression(MacroFunc(this=field), comments=comments)
 256
 257        if field is None:
 258            return None
 259
 260        if field.is_string or (isinstance(field, exp.Identifier) and field.quoted):
 261            return self.expression(
 262                MacroStrReplace(this=exp.Literal.string(field.this)), comments=comments
 263            )
 264
 265        if "@" in field.this:
 266            return field  # type: ignore[return-value]
 267        return self.expression(MacroVar(this=field.this), comments=comments)
 268
 269    if isinstance(field, (exp.Window, exp.IgnoreNulls, exp.RespectNulls)):
 270        field.set("this", _build_macro(field.this))
 271    else:
 272        field = _build_macro(field)
 273
 274    return field
 275
 276
 277KEYWORD_MACROS = {"WITH", "JOIN", "WHERE", "GROUP_BY", "HAVING", "ORDER_BY", "LIMIT"}
 278
 279
 280def _parse_matching_macro(self: Parser, name: str) -> t.Optional[exp.Expr]:
 281    if not self._match_pair(TokenType.PARAMETER, TokenType.VAR, advance=False) or (
 282        self._next and self._next.text.upper() != name.upper()
 283    ):
 284        return None
 285
 286    self._advance()
 287    return _parse_macro(self, keyword_macro=name)
 288
 289
 290def _parse_body_macro(self: Parser) -> t.Tuple[str, t.Optional[exp.Expr]]:
 291    name = self._next and self._next.text.upper()
 292
 293    if name == "JOIN":
 294        return ("joins", self._parse_join())
 295    if name == "WHERE":
 296        return ("where", self._parse_where())
 297    if name == "GROUP_BY":
 298        return ("group", self._parse_group())
 299    if name == "HAVING":
 300        return ("having", self._parse_having())
 301    if name == "ORDER_BY":
 302        return ("order", self._parse_order())
 303    if name == "LIMIT":
 304        return ("limit", self._parse_limit())
 305    return ("", None)
 306
 307
 308def _parse_with(self: Parser, skip_with_token: bool = False) -> t.Optional[exp.Expr]:
 309    macro = _parse_matching_macro(self, "WITH")
 310    if not macro:
 311        return self.__parse_with(skip_with_token=skip_with_token)  # type: ignore
 312
 313    macro.this.append("expressions", self.__parse_with(skip_with_token=True))  # type: ignore
 314    return macro
 315
 316
 317def _parse_join(
 318    self: Parser, skip_join_token: bool = False, parse_bracket: bool = False
 319) -> t.Optional[exp.Expr]:
 320    index = self._index
 321    method, side, kind = self._parse_join_parts()
 322    macro = _parse_matching_macro(self, "JOIN")
 323    if not macro:
 324        self._retreat(index)
 325        return self.__parse_join(skip_join_token=skip_join_token, parse_bracket=parse_bracket)  # type: ignore
 326
 327    join = self.__parse_join(skip_join_token=True)  # type: ignore
 328    if method:
 329        join.set("method", method.text)
 330    if side:
 331        join.set("side", side.text)
 332    if kind:
 333        join.set("kind", kind.text)
 334
 335    macro.this.append("expressions", join)
 336    return macro
 337
 338
 339def _warn_unsupported(self: Parser) -> None:
 340    from sqlmesh.core.console import get_console
 341
 342    sql = self._find_sql(self._tokens[0], self._tokens[-1])[: self.error_message_context]
 343
 344    get_console().log_warning(
 345        f"'{sql}' could not be semantically understood as it contains unsupported syntax, SQLMesh will treat the command as is. Note that any references to the model's "
 346        "underlying physical table can't be resolved in this case, consider using Jinja as explained here https://sqlmesh.readthedocs.io/en/stable/concepts/macros/macro_variables/#audit-only-variables"
 347    )
 348
 349
 350def _parse_select(
 351    self: Parser,
 352    nested: bool = False,
 353    table: bool = False,
 354    parse_subquery_alias: bool = True,
 355    parse_set_operation: bool = True,
 356    consume_pipe: bool = True,
 357    from_: t.Optional[exp.From] = None,
 358) -> t.Optional[exp.Expr]:
 359    select = self.__parse_select(  # type: ignore
 360        nested=nested,
 361        table=table,
 362        parse_subquery_alias=parse_subquery_alias,
 363        parse_set_operation=parse_set_operation,
 364        consume_pipe=consume_pipe,
 365        from_=from_,
 366    )
 367
 368    if (
 369        not select
 370        and not parse_set_operation
 371        and self._match_pair(TokenType.PARAMETER, TokenType.VAR, advance=False)
 372    ):
 373        self._advance()
 374        return _parse_macro(self)
 375
 376    return select
 377
 378
 379def _parse_where(self: Parser, skip_where_token: bool = False) -> t.Optional[exp.Expr]:
 380    macro = _parse_matching_macro(self, "WHERE")
 381    if not macro:
 382        return self.__parse_where(skip_where_token=skip_where_token)  # type: ignore
 383
 384    macro.this.append("expressions", self.__parse_where(skip_where_token=True))  # type: ignore
 385    return macro
 386
 387
 388def _parse_group(self: Parser, skip_group_by_token: bool = False) -> t.Optional[exp.Expr]:
 389    macro = _parse_matching_macro(self, "GROUP_BY")
 390    if not macro:
 391        return self.__parse_group(skip_group_by_token=skip_group_by_token)  # type: ignore
 392
 393    macro.this.append("expressions", self.__parse_group(skip_group_by_token=True))  # type: ignore
 394    return macro
 395
 396
 397def _parse_having(self: Parser, skip_having_token: bool = False) -> t.Optional[exp.Expr]:
 398    macro = _parse_matching_macro(self, "HAVING")
 399    if not macro:
 400        return self.__parse_having(skip_having_token=skip_having_token)  # type: ignore
 401
 402    macro.this.append("expressions", self.__parse_having(skip_having_token=True))  # type: ignore
 403    return macro
 404
 405
 406def _parse_order(
 407    self: Parser, this: t.Optional[exp.Expr] = None, skip_order_token: bool = False
 408) -> t.Optional[exp.Expr]:
 409    macro = _parse_matching_macro(self, "ORDER_BY")
 410    if not macro:
 411        return self.__parse_order(this, skip_order_token=skip_order_token)  # type: ignore
 412
 413    macro.this.append("expressions", self.__parse_order(this, skip_order_token=True))  # type: ignore
 414    return macro
 415
 416
 417def _parse_limit(
 418    self: Parser,
 419    this: t.Optional[exp.Expr] = None,
 420    top: bool = False,
 421    skip_limit_token: bool = False,
 422) -> t.Optional[exp.Expr]:
 423    macro = _parse_matching_macro(self, "TOP" if top else "LIMIT")
 424    if not macro:
 425        return self.__parse_limit(this, top=top, skip_limit_token=skip_limit_token)  # type: ignore
 426
 427    macro.this.append("expressions", self.__parse_limit(this, top=top, skip_limit_token=True))  # type: ignore
 428    return macro
 429
 430
 431def _parse_value(self: Parser, values: bool = True) -> t.Optional[exp.Expr]:
 432    wrapped = self._match(TokenType.L_PAREN, advance=False)
 433
 434    # The base _parse_value method always constructs a Tuple instance. This is problematic when
 435    # generating values with a macro function, because it's impossible to tell whether the user's
 436    # intention was to construct a row or a column with the VALUES expression. To avoid this, we
 437    # amend the AST such that the Tuple is replaced by the macro function call itself.
 438    expr = self.__parse_value()  # type: ignore
 439    if expr and not wrapped and isinstance(seq_get(expr.expressions, 0), MacroFunc):
 440        return expr.expressions[0]
 441
 442    return expr
 443
 444
 445def _parse_macro_or_clause(self: Parser, parser: t.Callable) -> t.Optional[exp.Expr]:
 446    return _parse_macro(self) if self._match(TokenType.PARAMETER) else parser()
 447
 448
 449def _parse_props(self: Parser) -> t.Optional[exp.Expr]:
 450    key = self._parse_id_var(any_token=True)
 451    if not key:
 452        return None
 453
 454    name = key.name.lower()
 455    if name == "time_data_type":
 456        # TODO: if we make *_data_type a convention to parse things into exp.DataType, we could make this more generic
 457        value = self._parse_types(schema=True)
 458    elif name == "when_matched":
 459        # Parentheses around the WHEN clauses can be used to disambiguate them from other properties
 460        value = self._parse_wrapped(
 461            lambda: _parse_macro_or_clause(self, self._parse_when_matched),
 462            optional=True,
 463        )
 464    elif name == "merge_filter":
 465        value = self._parse_conjunction()
 466    elif self._match(TokenType.L_PAREN):
 467        value = self.expression(exp.Tuple(expressions=self._parse_csv(self._parse_equality)))
 468        self._match_r_paren()
 469    else:
 470        value = self._parse_bracket(self._parse_field(any_token=True))
 471
 472    if name == "path" and value:
 473        # Make sure if we get a windows path that it is converted to posix
 474        value = exp.Literal.string(value.this.replace("\\", "/"))  # type: ignore
 475
 476    return self.expression(exp.Property(this=name, value=value))
 477
 478
 479def _parse_types(
 480    self: Parser,
 481    check_func: bool = False,
 482    schema: bool = False,
 483    allow_identifiers: bool = True,
 484    with_collation: bool = False,
 485) -> t.Optional[exp.Expr]:
 486    start = self._curr
 487    parsed_type = self.__parse_types(  # type: ignore
 488        check_func=check_func,
 489        schema=schema,
 490        allow_identifiers=allow_identifiers,
 491        with_collation=with_collation,
 492    )
 493
 494    if schema and parsed_type:
 495        parsed_type.meta["sql"] = self._find_sql(start, self._prev)
 496
 497    return parsed_type
 498
 499
 500# Only needed for Snowflake: its "staged file" syntax (@<path>) clashes with our macro
 501# var syntax. By converting the Var representation to a MacroVar, we should be able to
 502# handle both use cases: if there's no value in the MacroEvaluator's context for that
 503# MacroVar, it'll render into @<path>, so it won't break staged file path references.
 504#
 505# See: https://docs.snowflake.com/en/user-guide/querying-stage
 506def _parse_table_parts(
 507    self: Parser,
 508    schema: bool = False,
 509    is_db_reference: bool = False,
 510    wildcard: bool = False,
 511    fast: bool = False,
 512) -> exp.Table | StagedFilePath:
 513    index = self._index
 514    table = self.__parse_table_parts(  # type: ignore
 515        schema=schema, is_db_reference=is_db_reference, wildcard=wildcard, fast=fast
 516    )
 517
 518    if table is None:
 519        return table  # type: ignore[return-value]
 520
 521    table_arg = table.this
 522    name = table_arg.name if isinstance(table_arg, exp.Var) else ""
 523
 524    if name.startswith(SQLMESH_MACRO_PREFIX):
 525        # In these cases, we don't want to produce a `StagedFilePath` node:
 526        #
 527        # - @'...' needs to parsed as a string template
 528        # - @{foo}.bar needs to be parsed as a table with a macro var part
 529        # - @name(arg1 [, arg2 ...]) needs to be parsed as a macro function call
 530        #
 531        # These cases can unambiguously be parsed using the base `_parse_table_parts`, as there
 532        # is no overlap with staged files https://docs.snowflake.com/en/user-guide/querying-stage
 533        if (
 534            self._prev.token_type == TokenType.STRING
 535            or "{" in name
 536            or (
 537                self._curr
 538                and self._prev.token_type in (TokenType.L_PAREN, TokenType.R_PAREN)
 539                and self._curr.text.upper() not in ("FILE_FORMAT", "PATTERN")
 540                and not (table.args.get("format") or table.args.get("pattern"))
 541            )
 542        ):
 543            self._retreat(index)
 544            return Parser._parse_table_parts(
 545                self, schema=schema, is_db_reference=is_db_reference, fast=fast
 546            )  # type: ignore[return-value]
 547
 548        table_arg.replace(MacroVar(this=name[1:]))
 549        return StagedFilePath(**table.args)
 550
 551    return table
 552
 553
 554# Only needed for T-SQL: it spells a column's nullability right after its type, e.g.
 555# ALTER TABLE t ALTER COLUMN c INT NOT NULL. Without this the trailing clause is left
 556# over, so the whole statement falls back to a Command and any macros it contains (such as
 557# @this_model) are no longer resolved, which means they reach the engine verbatim.
 558#
 559# See: https://learn.microsoft.com/en-us/sql/t-sql/statements/alter-table-transact-sql
 560def _parse_alter_table_alter(self: Parser) -> t.Optional[exp.Expr]:
 561    alter_column = self.__parse_alter_table_alter()  # type: ignore
 562
 563    if isinstance(alter_column, exp.AlterColumn) and alter_column.args.get("dtype"):
 564        if self._match_pair(TokenType.NOT, TokenType.NULL):
 565            alter_column.set("allow_null", False)
 566        elif self._match(TokenType.NULL):
 567            alter_column.set("allow_null", True)
 568
 569    return alter_column
 570
 571
 572def altercolumn_sql(self: Generator, expression: exp.AlterColumn) -> str:
 573    sql = self._altercolumn_sql(expression)  # type: ignore
 574
 575    # sqlglot's generator returns as soon as it renders the type, so the nullability parsed
 576    # above has to be appended here
 577    allow_null = expression.args.get("allow_null")
 578    if expression.args.get("dtype") and allow_null is not None:
 579        sql = f"{sql} NULL" if allow_null else f"{sql} NOT NULL"
 580
 581    return sql
 582
 583
 584def _parse_if(self: Parser) -> t.Optional[exp.Expr]:
 585    # If we fail to parse an IF function with expressions as arguments, we then try
 586    # to parse a statement / command to support the macro @IF(condition, statement)
 587    index = self._index
 588    try:
 589        if self.dialect == "tsql":
 590            if not (self._index >= 2 and self._tokens[self._index - 2].text == "@"):
 591                return self.__parse_if()  # type: ignore
 592            return Parser.__parse_if(self)  # type: ignore
 593        return self.__parse_if()  # type: ignore
 594    except ParseError:
 595        self._retreat(index)
 596        self._match_l_paren()
 597
 598        cond = self._parse_conjunction()
 599        self._match(TokenType.COMMA)
 600
 601        # Try to parse a known statement, otherwise fall back to parsing a command
 602        # Since the trailing `)` token is not expected by the statement parsers, we
 603        # remove it from the token stream before trying to parse the statement.
 604        last_token = self._tokens[-1]
 605        if last_token.token_type == TokenType.R_PAREN:
 606            self._tokens[-2].comments.extend(last_token.comments)
 607            self._tokens.pop()
 608            if hasattr(self, "_tokens_size"):
 609                # keep _tokens_size in sync sqlglot 30.0.3 caches len(_tokens)
 610                # _advance() tries to read tokens[index + 1] past the new end
 611                self._tokens_size -= 1
 612        else:
 613            self.raise_error("Expecting )")
 614
 615        index = self._index
 616        stmt = self._parse_statement()
 617        if self._curr:
 618            self._retreat(index)
 619            stmt = self._parse_as_command(self._tokens[index])
 620
 621        return exp.Anonymous(this="IF", expressions=[cond, stmt])
 622
 623
 624def _create_parser(expression_type: t.Type[exp.Expr], table_keys: t.List[str]) -> t.Callable:
 625    def parse(self: Parser) -> t.Optional[exp.Expr]:
 626        from sqlmesh.core.model.kind import ModelKindName
 627
 628        expressions: t.List[exp.Expr] = []
 629
 630        while True:
 631            prev_property = seq_get(expressions, -1)
 632            if not self._match(TokenType.COMMA, expression=prev_property) and expressions:
 633                break
 634
 635            key_expression = self._parse_id_var(any_token=True)
 636            if not key_expression:
 637                break
 638
 639            # This allows macro functions that programmaticaly generate the property key-value pair
 640            if isinstance(key_expression, MacroFunc):
 641                expressions.append(key_expression)
 642                continue
 643
 644            key = key_expression.name.lower()
 645
 646            start = self._curr
 647            value: t.Optional[exp.Expr | str]
 648
 649            if key in table_keys:
 650                value = self._parse_table_parts()
 651                if value and self._prev.token_type == TokenType.STRING:
 652                    self.raise_error(
 653                        f"'{key}' property cannot be a string value: {value}. "
 654                        "Please use the identifier syntax instead, e.g. foo.bar instead of 'foo.bar'"
 655                    )
 656            elif key == "columns":
 657                value = self._parse_schema()
 658            elif key == "kind":
 659                field = _parse_macro_or_clause(self, lambda: self._parse_id_var(any_token=True))
 660
 661                if not field or isinstance(field, (MacroVar, MacroFunc)):
 662                    value = field
 663                else:
 664                    try:
 665                        kind = ModelKindName[field.name.upper()]
 666                    except KeyError:
 667                        raise SQLMeshError(
 668                            f"Model kind specified as '{field.name}', but that is not a valid model kind.\n\nPlease specify one of {', '.join(ModelKindName)}."
 669                        )
 670
 671                    if kind in (
 672                        ModelKindName.INCREMENTAL_BY_TIME_RANGE,
 673                        ModelKindName.INCREMENTAL_BY_UNIQUE_KEY,
 674                        ModelKindName.INCREMENTAL_BY_PARTITION,
 675                        ModelKindName.INCREMENTAL_UNMANAGED,
 676                        ModelKindName.SEED,
 677                        ModelKindName.VIEW,
 678                        ModelKindName.SCD_TYPE_2,
 679                        ModelKindName.SCD_TYPE_2_BY_TIME,
 680                        ModelKindName.SCD_TYPE_2_BY_COLUMN,
 681                        ModelKindName.CUSTOM,
 682                    ) and self._match(TokenType.L_PAREN, advance=False):
 683                        props = self._parse_wrapped_csv(functools.partial(_parse_props, self))
 684                    else:
 685                        props = None
 686
 687                    value = self.expression(ModelKind(this=kind.value, expressions=props))
 688            elif key == "expression":
 689                value = self._parse_conjunction()
 690            elif key == "partitioned_by":
 691                partitioned_by = self._parse_partitioned_by()
 692                if isinstance(partitioned_by.this, exp.Schema):
 693                    value = exp.tuple_(*partitioned_by.this.expressions)
 694                else:
 695                    value = partitioned_by.this
 696            elif key == "clustered_by":
 697                # Bare AUTO / NONE are Databricks liquid clustering keywords, not column refs.
 698                # Detect keywords by token type: unquoted bare identifiers arrive as VAR tokens.
 699                # Backtick-quoted identifiers (e.g. `auto`) have IDENTIFIER token type and are
 700                # treated as real column names.
 701                if (
 702                    self._curr is not None
 703                    and self._curr.token_type == TokenType.VAR
 704                    and self._curr.text.upper() in LIQUID_CLUSTERING_KEYWORDS
 705                ):
 706                    value = exp.Var(this=self._curr.text.upper())
 707                    self._advance()
 708                else:
 709                    parsed = self._parse_bracket(self._parse_field(any_token=True))
 710                    # Unwrap Paren wrapping a bare column to match partitioned_by normalisation:
 711                    # clustered_by (a) → stored as Column(a), not Paren(Column(a)).
 712                    # Preserve parens around function expressions: (TO_DATE(col)) stays as-is.
 713                    if isinstance(parsed, exp.Paren) and isinstance(parsed.this, exp.Column):
 714                        value = parsed.unnest()
 715                    else:
 716                        value = parsed
 717            else:
 718                value = self._parse_bracket(self._parse_field(any_token=True))
 719
 720            if isinstance(value, exp.Expr):
 721                value.meta["sql"] = self._find_sql(start, self._prev)
 722
 723            expressions.append(self.expression(exp.Property(this=key, value=value)))
 724
 725        return self.expression(expression_type(expressions=expressions))
 726
 727    return parse
 728
 729
 730PARSERS = {
 731    "MODEL": _create_parser(Model, ["name"]),
 732    "AUDIT": _create_parser(Audit, ["model"]),
 733    "METRIC": _create_parser(Metric, ["name"]),
 734}
 735
 736
 737_SQLMESH_META_DIALECT = "sqlmesh_meta_dialect"
 738
 739
 740def _holds_expression(annotation: t.Any, _visited: t.Optional[t.FrozenSet[t.Any]] = None) -> bool:
 741    """Whether a declared field type bottoms out in a SQLGlot expression.
 742
 743    Covers List[exp.Expr], Optional[Dict[str, exp.DataType]], Optional[exp.Tuple], the
 744    nested Tuple[str, Dict[str, exp.Expr]] shape used by audits/signals, and nested
 745    Pydantic models that themselves wrap an expression field, such as `TimeColumn`
 746    (IncrementalByTimeRangeKind.time_column).
 747
 748    Stops at `_ModelKind` subclasses without recursing into their fields: a `kind`
 749    property's own nested properties are independently dialect-tagged via the
 750    `ModelKind` expression node's own meta when `_props_sql` recurses into them, so
 751    treating the `kind` field itself as "holds an expression" -- true only because some
 752    other member of the `ModelKind` union has an expression field, e.g.
 753    `IncrementalByTimeRangeKind.time_column` -- would route its entire subtree,
 754    including scalar sibling properties like `forward_only`, through a dialect-specific
 755    generator and transpile them when they shouldn't be (tsql booleans becoming
 756    `(1 = 1)`, which silently reparses as `False`).
 757    """
 758    from sqlmesh.core.model.kind import _ModelKind
 759
 760    if isinstance(annotation, type):
 761        if issubclass(annotation, exp.Expr):
 762            return True
 763        if issubclass(annotation, _ModelKind):
 764            return False
 765        visited = _visited or frozenset()
 766        if annotation in visited:
 767            return False
 768        if hasattr(annotation, "model_fields"):
 769            visited = visited | {annotation}
 770            return any(
 771                _holds_expression(field.annotation, visited)
 772                for field in annotation.model_fields.values()
 773            )
 774        return False
 775    return any(_holds_expression(arg, _visited) for arg in t.get_args(annotation))
 776
 777
 778@functools.lru_cache(maxsize=1)
 779def _meta_render_policy() -> t.Dict[str, bool]:
 780    """Map header property name -> whether its value is warehouse SQL.
 781
 782    Derived from the field declarations themselves, so it stays correct as properties
 783    are added: expression-typed values (columns, audits, physical_properties, ...) are
 784    the user's warehouse SQL and must render in the model's dialect, while scalar-typed
 785    values (allow_partials, description, kind, ...) are SQLMesh's own semantics and must
 786    stay dialect-agnostic -- transpiling those is what corrupts `allow_partials TRUE`
 787    into tsql's unparseable `(1 = 1)`.
 788    """
 789    import inspect
 790
 791    from sqlmesh.core.audit.definition import ModelAudit
 792    from sqlmesh.core.metric.definition import MetricMeta
 793    from sqlmesh.core.model import kind as kind_module
 794    from sqlmesh.core.model.meta import ModelMeta
 795
 796    sources: t.List[t.Any] = [ModelMeta, ModelAudit, MetricMeta]
 797    sources.extend(
 798        obj
 799        for name, obj in vars(kind_module).items()
 800        if inspect.isclass(obj) and hasattr(obj, "model_fields") and name.endswith("Kind")
 801    )
 802
 803    policy: t.Dict[str, bool] = {}
 804    for source in sources:
 805        for name, field in source.model_fields.items():
 806            policy.setdefault((field.alias or name).lower(), _holds_expression(field.annotation))
 807
 808    # `ModelMeta._pre_root_validator` (sqlmesh/core/model/meta.py) renames these two
 809    # user-facing property names to their target field before Pydantic validation, so
 810    # they never surface as a `Field(alias=...)` for the reflection above to find. Give
 811    # each the render policy of the field it is renamed to.
 812    pre_validator_aliases = {
 813        "grain": "grains",
 814        "table_properties": "physical_properties",
 815    }
 816    for alias, target in pre_validator_aliases.items():
 817        if target in policy:
 818            policy[alias] = policy[target]
 819
 820    return policy
 821
 822
 823@functools.lru_cache(maxsize=None)
 824def _dialect_renders_array_as_brackets(dialect_name: t.Optional[str]) -> bool:
 825    """Whether `dialect_name`'s own generator spells an array literal as `[a, b]`.
 826
 827    Checked by actually rendering a sample `exp.Array` with that dialect, rather than
 828    inspecting `Dialect.ARRAY_SIZE_NAME` or similar generator flags, because the
 829    generator is the single source of truth for what a dialect's array syntax looks
 830    like and there is no single shared flag for it across dialects. This also covers
 831    dialects (tsql, sqlite, tableau, exasol, fabric) that reuse `[`/`]` for identifier
 832    quoting and therefore render arrays as `ARRAY(...)` instead: rewriting their
 833    `tags`/`ignored_rules` value to `[a, b]` would not be an array literal in their
 834    grammar at all, so it silently reparses as one bracket-quoted identifier and
 835    corrupts the value. An unrecognized dialect name renders with the generic
 836    generator, which itself does not use brackets, so it falls back to `False`.
 837    """
 838    try:
 839        sample = exp.Array(expressions=[exp.Literal.string("x")])
 840        return sample.sql(dialect=dialect_name).startswith("[")
 841    except Exception:
 842        return False
 843
 844
 845def _props_sql(self: Generator, expressions: t.List[exp.Expr]) -> str:
 846    props = []
 847    size = len(expressions)
 848
 849    for i, prop in enumerate(expressions):
 850        parent = prop.parent
 851        meta_dialect = parent.meta.get(_SQLMESH_META_DIALECT) if parent else None
 852
 853        def render_with_model_dialect(node: exp.Expr, **overrides: t.Any) -> str:
 854            opts: t.Dict[str, t.Any] = {
 855                "dialect": meta_dialect,
 856                "pretty": self.pretty,
 857                "identify": self.identify,
 858                "normalize": self.normalize,
 859                "pad": self.pad,
 860                "indent": self._indent,
 861                "normalize_functions": self.normalize_functions,
 862                "leading_comma": self.leading_comma,
 863                "max_text_width": self.max_text_width,
 864                "comments": self.comments,
 865            }
 866            opts.update(overrides)
 867
 868            # Keep boolean literals anywhere in the value (audit args, physical_properties,
 869            # merge_filter, ...) as `TRUE`/`FALSE`: tsql would otherwise emit `(1 = 1)`,
 870            # which reformats differently on the next pass. The value is transpiled with
 871            # the model dialect anyway when it is used, e.g. in the rendered audit query.
 872            def keep_boolean_literal(n: exp.Expr) -> exp.Expr:
 873                if not isinstance(n, exp.Boolean):
 874                    return n
 875                literal = exp.var("TRUE" if n.this else "FALSE")
 876                literal.comments = n.comments
 877                return literal
 878
 879            return node.transform(keep_boolean_literal).sql(**opts)
 880
 881        if isinstance(prop, MacroFunc):
 882            # A macro in property position wraps user-authored arguments, so it carries
 883            # warehouse SQL the same way `columns` or `audits` do. Clear the outer node's
 884            # own comments (not `.this`'s, which `_macro_func_sql` already attaches)
 885            # before rendering with the model dialect, mirroring what `comment=False`
 886            # does for the non-dialect path below -- passing `comments=False` here
 887            # instead would build a fresh Generator with comments globally disabled,
 888            # silently dropping every comment in the subtree rather than just the
 889            # redundant outer one.
 890            if meta_dialect:
 891                prop_for_render = prop.copy()
 892                prop_for_render.comments = None
 893                sql = self.indent(render_with_model_dialect(prop_for_render))
 894            else:
 895                sql = self.indent(self.sql(prop, comment=False))
 896        else:
 897            value = prop.args.get("value")
 898
 899            if (
 900                meta_dialect
 901                and isinstance(value, exp.Expr)
 902                and _meta_render_policy().get(prop.name.lower())
 903            ):
 904                value_sql = render_with_model_dialect(value)
 905            elif (
 906                meta_dialect
 907                and isinstance(value, exp.Array)
 908                and _dialect_renders_array_as_brackets(meta_dialect)
 909            ):
 910                # Dialect-agnostic properties (e.g. `tags`, `ignored_rules`) that hold a
 911                # list still go through the base (dialect=None) generator, which renders
 912                # an `exp.Array` as `ARRAY(...)`. On BigQuery `ARRAY(` is parsed as a
 913                # subquery constructor, so a multi-element `ARRAY('a', 'b')` fails to
 914                # reparse ("Required keyword: 'value' missing for Property"). Render it
 915                # as a bracketed list literal instead -- but only for dialects that
 916                # actually spell arrays that way; dialects that reuse `[`/`]` for
 917                # identifier quoting (tsql, sqlite, ...) keep the generic `ARRAY(...)`
 918                # form, which they parse back correctly. The elements themselves stay on
 919                # the dialect-agnostic path (`self.expressions`, not
 920                # `render_with_model_dialect`): these are SQLMesh's own scalar values
 921                # (tag/rule name strings), not user warehouse SQL, so they must not be
 922                # transpiled with the model dialect (e.g. tsql boolean literals turning
 923                # into `(1 = 1)`).
 924                value_sql = f"[{self.expressions(value, flat=True)}]"
 925            else:
 926                value_sql = self.sql(prop, "value")
 927
 928            sql = self.indent(f"{prop.name} {value_sql}")
 929
 930        if i < size - 1:
 931            sql += ","
 932
 933        props.append(self.maybe_comment(sql, expression=prop))
 934
 935    return "\n".join(props)
 936
 937
 938def _on_virtual_update_sql(self: Generator, expressions: t.List[exp.Expr]) -> str:
 939    statements = "\n".join(
 940        self.sql(expression)
 941        if isinstance(expression, JinjaStatement)
 942        else f"{self.sql(expression)};"
 943        for expression in expressions
 944    )
 945    return f"{ON_VIRTUAL_UPDATE_BEGIN};\n{statements}\n{ON_VIRTUAL_UPDATE_END};"
 946
 947
 948def _sqlmesh_ddl_sql(self: Generator, expression: Model | Audit | Metric, name: str) -> str:
 949    return "\n".join([f"{name} (", _props_sql(self, expression.expressions), ")"])
 950
 951
 952def _model_kind_sql(self: Generator, expression: ModelKind) -> str:
 953    props = _props_sql(self, expression.expressions)
 954    if props:
 955        return "\n".join([f"{expression.this} (", props, ")"])
 956    return expression.name.upper()
 957
 958
 959def _macro_keyword_func_sql(self: Generator, expression: exp.Expr) -> str:
 960    name = expression.name
 961    keyword = name.replace("_", " ")
 962    *args, clause = expression.expressions
 963    macro = f"@{name}({self.format_args(*args)})"
 964    return self.sql(clause).replace(keyword, macro, 1)
 965
 966
 967def _macro_func_sql(self: Generator, expression: MacroFunc) -> str:
 968    expression = expression.this
 969    name = expression.name
 970    if name in KEYWORD_MACROS:
 971        sql = _macro_keyword_func_sql(self, expression)
 972    else:
 973        sql = f"@{name}({self.format_args(*expression.expressions)})"
 974    return self.maybe_comment(sql, expression)
 975
 976
 977def _whens_sql(self: Generator, expression: exp.Whens) -> str:
 978    if isinstance(expression.parent, exp.Merge):
 979        return self.whens_sql(expression)
 980
 981    # If the `WHEN` clauses aren't part of a MERGE statement (e.g. they
 982    # appear in the `MODEL` DDL), then we will wrap them with parentheses.
 983    return self.wrap(self.expressions(expression, sep=" ", indent=False))
 984
 985
 986def _parse_interval_span(self: Parser, this: exp.Expr) -> exp.Interval:
 987    interval = self.__parse_interval_span(this)  # type: ignore
 988    # Without this, @unit in `INTERVAL @value @unit` is misread as an alias.
 989    if not interval.args.get("unit") and self._match(TokenType.PARAMETER):
 990        macro = _parse_macro(self)
 991        if macro is not None:
 992            interval.set("unit", macro)
 993    return interval
 994
 995
 996def _override(klass: t.Type[Tokenizer | Parser | Generator], func: t.Callable) -> None:
 997    name = func.__name__
 998    if getattr(klass, name, None) is func:
 999        # Already overridden. Re-applying would save the override itself as the
1000        # "original", making the wrapper call itself and recurse infinitely.
1001        return
1002    setattr(klass, f"_{name}", getattr(klass, name))
1003    setattr(klass, name, func)
1004
1005
1006def format_model_expressions(
1007    expressions: t.List[exp.Expr],
1008    dialect: t.Optional[str] = None,
1009    rewrite_casts: bool = True,
1010    normalize_functions: t.Union[str, bool, None] = False,
1011    **kwargs: t.Any,
1012) -> str:
1013    """Format a model's expressions into a standardized format.
1014
1015    Args:
1016        expressions: The model's expressions, must be at least model def + query.
1017        dialect: The dialect to render the expressions as.
1018        rewrite_casts: Whether to rewrite all casts to use the :: syntax.
1019        normalize_functions: How to normalize function name casing.
1020
1021            * ``False`` (default) — preserves the original spelling of custom and audit
1022              function names.  SQLGlot built-in functions may still canonicalize because
1023              the parser discards the original token.
1024            * ``"upper"`` — uppercases all function names including custom audit
1025              references.
1026            * ``"lower"`` — lowercases all function names including built-ins.
1027            * ``True`` — defers to SQLGlot's generator default (uppercase).
1028            * ``None`` — passes ``None`` directly to the SQLGlot generator, which
1029              defers to SQLGlot's own default (typically uppercase, but may vary by
1030              dialect).  Note: this is the **direct generator API** behaviour.  When
1031              called via ``FormatConfig``, ``None`` is excluded by Pydantic's
1032              ``exclude_none`` serialization and this function receives its own ``False``
1033              default instead — so the two paths are not equivalent.
1034        **kwargs: Additional keyword arguments to pass to the sql generator.
1035
1036    Returns:
1037        A string representing the formatted model.
1038    """
1039
1040    def tag_meta_dialect(expression: exp.Expr) -> exp.Expr:
1041        """Record the model dialect on meta nodes so `_props_sql` can render the
1042        warehouse-SQL properties (columns, audits, physical_properties, ...) with it
1043        while the SQLMesh-owned ones stay dialect-agnostic. Tags nested ModelKind
1044        nodes too, since kinds carry expression properties of their own such as
1045        `time_data_type` and `unique_key`."""
1046        if not dialect or not is_meta_expression(expression):
1047            return expression
1048
1049        expression = expression.copy()
1050        for node in expression.find_all(Model, Audit, Metric, ModelKind):
1051            node.meta[_SQLMESH_META_DIALECT] = dialect
1052        expression.meta[_SQLMESH_META_DIALECT] = dialect
1053        return expression
1054
1055    if len(expressions) == 1 and is_meta_expression(expressions[0]):
1056        # Meta expressions (MODEL/AUDIT/METRIC) are SQLMesh DDL, not standard SQL,
1057        # so they must never be transpiled to the target dialect (e.g. tsql would
1058        # rewrite a boolean property like `allow_partials TRUE` to `(1 = 1)`).
1059        # Individual properties whose values *are* warehouse SQL still render with
1060        # the model dialect -- see `_props_sql` / `_meta_render_policy`.
1061        return tag_meta_dialect(expressions[0]).sql(
1062            pretty=True, dialect=None, normalize_functions=normalize_functions
1063        )
1064
1065    if rewrite_casts:
1066
1067        def cast_to_colon(node: exp.Expr) -> exp.Expr:
1068            # Directly check type instead of isinstance to avoid rewriting subclasses of CAST, e.g. JSONCast
1069            if type(node) is exp.Cast and not any(
1070                # Only convert CAST into :: if it doesn't have additional args set, otherwise this
1071                # conversion could alter the semantics (eg. changing SAFE_CAST in BigQuery to CAST)
1072                arg
1073                for name, arg in node.args.items()
1074                if name not in ("this", "to")
1075            ):
1076                this = node.this
1077
1078                if not isinstance(this, (exp.Binary, exp.Unary)) or isinstance(this, exp.Paren):
1079                    cast = DColonCast(this=this, to=node.to)
1080                    cast.comments = node.comments
1081                    node = cast
1082
1083            exp.replace_children(node, cast_to_colon)
1084            return node
1085
1086        new_expressions = []
1087        for expression in expressions:
1088            expression = expression.copy()
1089            exp.replace_children(expression, cast_to_colon)
1090            new_expressions.append(expression)
1091
1092        expressions = new_expressions
1093
1094    return ";\n\n".join(
1095        # Meta expressions (MODEL/AUDIT/METRIC) are SQLMesh DDL and must stay
1096        # dialect-agnostic; only the actual query/statement expressions transpile.
1097        tag_meta_dialect(expression).sql(
1098            pretty=True,
1099            dialect=None if is_meta_expression(expression) else dialect,
1100            normalize_functions=normalize_functions,
1101            **kwargs,
1102        )
1103        for expression in expressions
1104    ).strip()
1105
1106
1107def text_diff(
1108    a: t.List[exp.Expr],
1109    b: t.List[exp.Expr],
1110    a_dialect: t.Optional[str] = None,
1111    b_dialect: t.Optional[str] = None,
1112) -> str:
1113    """Find the unified text diff between two expressions."""
1114    a_sql = [
1115        line
1116        for expr in a
1117        for line in expr.sql(pretty=True, comments=False, dialect=a_dialect).split("\n")
1118    ]
1119    b_sql = [
1120        line
1121        for expr in b
1122        for line in expr.sql(pretty=True, comments=False, dialect=b_dialect).split("\n")
1123    ]
1124    return "\n".join(unified_diff(a_sql, b_sql))
1125
1126
1127WS_OR_COMMENT = r"(?:\s|--[^\n]*\n|/\*.*?\*/)"
1128HEADER = r"\b(?:model|audit)\b(?=\s*\()"
1129KEY_BOUNDARY = r"(?:\(|,)"  # key is preceded by either '(' or ','
1130DIALECT_VALUE = r"['\"]?(?P<dialect>[a-z][a-z0-9]*)['\"]?"
1131VALUE_BOUNDARY = r"(?=,|\))"  # value is followed by comma or closing paren
1132
1133DIALECT_PATTERN = re.compile(
1134    rf"{HEADER}.*?{KEY_BOUNDARY}{WS_OR_COMMENT}*dialect{WS_OR_COMMENT}+{DIALECT_VALUE}{WS_OR_COMMENT}*{VALUE_BOUNDARY}",
1135    re.IGNORECASE | re.DOTALL,
1136)
1137
1138
1139def _is_command_statement(command: str, tokens: t.List[Token], pos: int) -> bool:
1140    try:
1141        return (
1142            tokens[pos].text.upper() == command.upper()
1143            and tokens[pos + 1].token_type == TokenType.SEMICOLON
1144        )
1145    except IndexError:
1146        return False
1147
1148
1149JINJA_QUERY_BEGIN = "JINJA_QUERY_BEGIN"
1150JINJA_STATEMENT_BEGIN = "JINJA_STATEMENT_BEGIN"
1151JINJA_END = "JINJA_END"
1152ON_VIRTUAL_UPDATE_BEGIN = "ON_VIRTUAL_UPDATE_BEGIN"
1153ON_VIRTUAL_UPDATE_END = "ON_VIRTUAL_UPDATE_END"
1154
1155
1156def _is_jinja_statement_begin(tokens: t.List[Token], pos: int) -> bool:
1157    return _is_command_statement(JINJA_STATEMENT_BEGIN, tokens, pos)
1158
1159
1160def _is_jinja_query_begin(tokens: t.List[Token], pos: int) -> bool:
1161    return _is_command_statement(JINJA_QUERY_BEGIN, tokens, pos)
1162
1163
1164def _is_jinja_end(tokens: t.List[Token], pos: int) -> bool:
1165    return _is_command_statement(JINJA_END, tokens, pos)
1166
1167
1168def jinja_query(query: str) -> JinjaQuery:
1169    return JinjaQuery(this=exp.Literal.string(query.strip()))
1170
1171
1172def jinja_statement(statement: str) -> JinjaStatement:
1173    return JinjaStatement(this=exp.Literal.string(statement.strip()))
1174
1175
1176def _is_virtual_statement_begin(tokens: t.List[Token], pos: int) -> bool:
1177    return _is_command_statement(ON_VIRTUAL_UPDATE_BEGIN, tokens, pos)
1178
1179
1180def _is_virtual_statement_end(tokens: t.List[Token], pos: int) -> bool:
1181    return _is_command_statement(ON_VIRTUAL_UPDATE_END, tokens, pos)
1182
1183
1184def virtual_statement(statements: t.List[exp.Expr]) -> VirtualUpdateStatement:
1185    return VirtualUpdateStatement(expressions=statements)
1186
1187
1188class ChunkType(Enum):
1189    JINJA_QUERY = auto()
1190    JINJA_STATEMENT = auto()
1191    SQL = auto()
1192    VIRTUAL_STATEMENT = auto()
1193    VIRTUAL_JINJA_STATEMENT = auto()
1194
1195
1196def parse_one(
1197    sql: str, dialect: t.Optional[str] = None, into: t.Optional[exp.IntoType] = None
1198) -> exp.Expr:
1199    expressions = parse(sql, default_dialect=dialect, match_dialect=False, into=into)
1200    if not expressions:
1201        raise SQLMeshError(f"No expressions found in '{sql}'")
1202    elif len(expressions) > 1:
1203        raise SQLMeshError(f"Multiple expressions found in '{sql}'")
1204    return expressions[0]
1205
1206
1207def parse(
1208    sql: str,
1209    default_dialect: t.Optional[str] = None,
1210    match_dialect: bool = True,
1211    into: t.Optional[exp.IntoType] = None,
1212) -> t.List[exp.Expr]:
1213    """Parse a sql string.
1214
1215    Supports parsing model definition.
1216    If a jinja block is detected, the query is stored as raw string in a Jinja node.
1217
1218    Args:
1219        sql: The sql based definition.
1220        default_dialect: The dialect to use if the model does not specify one.
1221
1222    Returns:
1223        A list of the parsed expressions: [Model, *Statements, Query, *Statements]
1224    """
1225    match = match_dialect and DIALECT_PATTERN.search(sql[:MAX_MODEL_DEFINITION_SIZE])
1226    dialect_str = match.group("dialect") if match else None
1227    dialect = Dialect.get_or_raise(dialect_str or default_dialect)
1228
1229    tokens = dialect.tokenize(sql)
1230    chunks: t.List[t.Tuple[t.List[Token], ChunkType]] = [([], ChunkType.SQL)]
1231    total = len(tokens)
1232
1233    pos = 0
1234    virtual = False
1235    while pos < total:
1236        token = tokens[pos]
1237        if _is_virtual_statement_end(tokens, pos):
1238            chunks[-1][0].append(token)
1239            virtual = False
1240            chunks.append(([], ChunkType.SQL))
1241            pos += 2
1242        elif _is_jinja_end(tokens, pos) or (
1243            chunks[-1][1] == ChunkType.SQL
1244            and token.token_type == TokenType.SEMICOLON
1245            and pos < total - 1
1246        ):
1247            if token.token_type == TokenType.SEMICOLON:
1248                pos += 1
1249            else:
1250                # Jinja end statement
1251                chunks[-1][0].append(token)
1252                pos += 2
1253            chunks.append(
1254                (
1255                    [],
1256                    ChunkType.VIRTUAL_STATEMENT
1257                    if virtual and tokens[pos] != ON_VIRTUAL_UPDATE_END
1258                    else ChunkType.SQL,
1259                )
1260            )
1261        elif _is_jinja_query_begin(tokens, pos):
1262            chunks.append(([token], ChunkType.JINJA_QUERY))
1263            pos += 2
1264        elif _is_jinja_statement_begin(tokens, pos):
1265            chunks.append(([token], ChunkType.JINJA_STATEMENT))
1266            pos += 2
1267        elif _is_virtual_statement_begin(tokens, pos):
1268            chunks.append(([token], ChunkType.VIRTUAL_STATEMENT))
1269            pos += 2
1270            virtual = True
1271        else:
1272            chunks[-1][0].append(token)
1273            pos += 1
1274
1275    parser = dialect.parser()
1276    expressions: t.List[exp.Expr] = []
1277
1278    def parse_sql_chunk(chunk: t.List[Token], meta_sql: bool = True) -> t.List[exp.Expr]:
1279        parsed_expressions: t.List[t.Optional[exp.Expr]] = (
1280            parser.parse(chunk, sql) if into is None else parser.parse_into(into, chunk, sql)
1281        )
1282        expressions = []
1283        for expression in parsed_expressions:
1284            if expression:
1285                if meta_sql:
1286                    expression.meta["sql"] = parser._find_sql(chunk[0], chunk[-1])
1287                expressions.append(expression)
1288        return expressions
1289
1290    def parse_jinja_chunk(chunk: t.List[Token], meta_sql: bool = True) -> exp.Expr:
1291        start, *_, end = chunk
1292        segment = sql[start.end + 2 : end.start - 1]
1293        factory = jinja_query if chunk_type == ChunkType.JINJA_QUERY else jinja_statement
1294        expression = factory(segment.strip())
1295        if meta_sql:
1296            expression.meta["sql"] = sql[start.start : end.end + 1]
1297        return expression
1298
1299    def parse_virtual_statement(
1300        chunks: t.List[t.Tuple[t.List[Token], ChunkType]], pos: int
1301    ) -> t.Tuple[t.List[exp.Expr], int]:
1302        # For virtual statements we need to handle both SQL and Jinja nested blocks within the chunk
1303        virtual_update_statements: t.List[exp.Expr] = []
1304        start = chunks[pos][0][0].start
1305
1306        while (
1307            chunks[pos - 1][0] == [] or chunks[pos - 1][0][-1].text.upper() != ON_VIRTUAL_UPDATE_END
1308        ):
1309            chunk, chunk_type = chunks[pos]
1310            if chunk_type == ChunkType.JINJA_STATEMENT:
1311                virtual_update_statements.append(parse_jinja_chunk(chunk, False))
1312            else:
1313                virtual_update_statements.extend(
1314                    parse_sql_chunk(
1315                        chunk[int(chunk[0].text.upper() == ON_VIRTUAL_UPDATE_BEGIN) : -1], False
1316                    ),
1317                )
1318            pos += 1
1319
1320        if virtual_update_statements:
1321            statements = virtual_statement(virtual_update_statements)
1322            end = chunk[-1].end + 1
1323            statements.meta["sql"] = sql[start:end]
1324            return [statements], pos
1325
1326        return [], pos
1327
1328    pos = 0
1329    total_chunks = len(chunks)
1330    while pos < total_chunks:
1331        chunk, chunk_type = chunks[pos]
1332        if chunk_type == ChunkType.VIRTUAL_STATEMENT:
1333            virtual_expression, pos = parse_virtual_statement(chunks, pos)
1334            expressions.extend(virtual_expression)
1335        elif chunk_type == ChunkType.SQL:
1336            expressions.extend(parse_sql_chunk(chunk))
1337        else:
1338            expressions.append(parse_jinja_chunk(chunk))
1339        pos += 1
1340
1341    return expressions
1342
1343
1344def extend_sqlglot() -> None:
1345    """Extend SQLGlot with SQLMesh's custom macro aware dialect."""
1346    tokenizers = {Tokenizer}
1347    parsers = {Parser}
1348    generators = {Generator}
1349
1350    for dialect in Dialect.classes.values():
1351        # Athena picks a different Tokenizer / Parser / Generator depending on the query
1352        # so this ensures that the extra ones it defines are also extended
1353        if dialect == athena.Athena:
1354            tokenizers.add(athena._TrinoTokenizer)
1355            parsers.add(AthenaTrinoParser)
1356            generators.add(athena_generators.AthenaTrinoGenerator)
1357            generators.add(athena_generators._HiveGenerator)
1358
1359        if hasattr(dialect, "Tokenizer"):
1360            tokenizers.add(dialect.Tokenizer)
1361        if hasattr(dialect, "Parser"):
1362            parsers.add(dialect.Parser)
1363        if hasattr(dialect, "Generator"):
1364            generators.add(dialect.Generator)
1365
1366    for tokenizer in tokenizers:
1367        tokenizer.VAR_SINGLE_TOKENS.update(SQLMESH_MACRO_PREFIX)
1368
1369    for parser in parsers:
1370        parser.FUNCTIONS.update({"JINJA": Jinja.from_arg_list, "METRIC": MetricAgg.from_arg_list})
1371        parser.PLACEHOLDER_PARSERS.update({TokenType.PARAMETER: _parse_macro})
1372        parser.QUERY_MODIFIER_PARSERS.update(
1373            {TokenType.PARAMETER: lambda self: _parse_body_macro(self)}
1374        )
1375
1376    for generator in generators:
1377        if MacroFunc not in generator.TRANSFORMS:
1378            generator.TRANSFORMS.update(
1379                {
1380                    Audit: lambda self, e: _sqlmesh_ddl_sql(self, e, "AUDIT"),
1381                    DColonCast: lambda self, e: f"{self.sql(e, 'this')}::{self.sql(e, 'to')}",
1382                    Jinja: lambda self, e: e.name,
1383                    JinjaQuery: lambda self, e: f"{JINJA_QUERY_BEGIN};\n{e.name}\n{JINJA_END};",
1384                    JinjaStatement: lambda self, e: (
1385                        f"{JINJA_STATEMENT_BEGIN};\n{e.name}\n{JINJA_END};"
1386                    ),
1387                    VirtualUpdateStatement: lambda self, e: _on_virtual_update_sql(self, e),
1388                    MacroDef: lambda self, e: f"@DEF({self.sql(e.this)}, {self.sql(e.expression)})",
1389                    MacroFunc: _macro_func_sql,
1390                    MacroStrReplace: lambda self, e: f"@{self.sql(e.this)}",
1391                    MacroSQL: lambda self, e: f"@SQL({self.sql(e.this)})",
1392                    MacroVar: lambda self, e: f"@{e.name}",
1393                    Metric: lambda self, e: _sqlmesh_ddl_sql(self, e, "METRIC"),
1394                    Model: lambda self, e: _sqlmesh_ddl_sql(self, e, "MODEL"),
1395                    ModelKind: _model_kind_sql,
1396                    PythonCode: lambda self, e: self.expressions(e, sep="\n", indent=False),
1397                    StagedFilePath: lambda self, e: self.table_sql(e),
1398                    exp.Whens: _whens_sql,
1399                }
1400            )
1401        if MacroDef not in generator.WITH_SEPARATED_COMMENTS:
1402            generator.WITH_SEPARATED_COMMENTS = (
1403                *generator.WITH_SEPARATED_COMMENTS,
1404                Model,
1405                MacroDef,
1406            )
1407
1408        if MacroVar not in generator.UNWRAPPED_INTERVAL_VALUES:
1409            generator.UNWRAPPED_INTERVAL_VALUES = (
1410                *generator.UNWRAPPED_INTERVAL_VALUES,
1411                MacroStrReplace,
1412                MacroVar,
1413            )
1414
1415    _override(Parser, _parse_select)
1416    _override(Parser, _parse_statement)
1417    _override(Parser, _parse_join)
1418    _override(Parser, _parse_order)
1419    _override(Parser, _parse_where)
1420    _override(Parser, _parse_group)
1421    _override(Parser, _parse_with)
1422    _override(Parser, _parse_having)
1423    _override(Parser, _parse_limit)
1424    _override(Parser, _parse_value)
1425    _override(Parser, _parse_lambda)
1426    _override(Parser, _parse_types)
1427    _override(Parser, _parse_if)
1428    _override(TSQL.Parser, Parser._parse_if)
1429    _override(Parser, _parse_id_var)
1430    _override(Parser, _parse_interval_span)
1431    _override(Parser, _warn_unsupported)
1432    _override(Snowflake.Parser, _parse_table_parts)
1433    _override(TSQL.Parser, _parse_alter_table_alter)
1434    _override(TSQL.Generator, altercolumn_sql)
1435
1436    # DuckDB's prefix absolute power operator `@` clashes with the macro syntax
1437    DuckDB.Parser.NO_PAREN_FUNCTION_PARSERS.pop("@", None)
1438
1439
1440def select_from_values(
1441    values: t.List[t.Tuple[t.Any, ...]],
1442    columns_to_types: t.Dict[str, exp.DataType],
1443    batch_size: int = 0,
1444    alias: str = "t",
1445) -> t.Iterator[exp.Select]:
1446    """Generate a VALUES expression that has a select wrapped around it to cast the values to their correct types.
1447
1448    Args:
1449        values: List of values to use for the VALUES expression.
1450        columns_to_types: Mapping of column names to types to assign to the values.
1451        batch_size: The maximum number of tuples per batches. Defaults to sys.maxsize if <= 0.
1452        alias: The alias to assign to the values expression. If not provided then will default to "t"
1453
1454    Returns:
1455        This method operates as a generator and yields a VALUES expression.
1456    """
1457    if batch_size <= 0:
1458        batch_size = sys.maxsize
1459    num_rows = len(values)
1460    for i in range(0, num_rows, batch_size):
1461        yield select_from_values_for_batch_range(
1462            values=values,
1463            target_columns_to_types=columns_to_types,
1464            batch_start=i,
1465            batch_end=min(i + batch_size, num_rows),
1466            alias=alias,
1467        )
1468
1469
1470def select_from_values_for_batch_range(
1471    values: t.List[t.Tuple[t.Any, ...]],
1472    target_columns_to_types: t.Dict[str, exp.DataType],
1473    batch_start: int,
1474    batch_end: int,
1475    alias: str = "t",
1476    source_columns: t.Optional[t.List[str]] = None,
1477) -> exp.Select:
1478    source_columns = source_columns or list(target_columns_to_types)
1479    source_columns_to_types = get_source_columns_to_types(target_columns_to_types, source_columns)
1480
1481    if not values:
1482        # Ensures we don't generate an empty VALUES clause & forces a zero-row output
1483        where = exp.false()
1484        expressions = [
1485            tuple(exp.cast(exp.null(), to=kind) for kind in source_columns_to_types.values())
1486        ]
1487    else:
1488        where = None
1489        expressions = [
1490            tuple(transform_values(v, source_columns_to_types))
1491            for v in values[batch_start:batch_end]
1492        ]
1493
1494    values_exp = exp.values(expressions, alias=alias, columns=source_columns_to_types)
1495    if values:
1496        # BigQuery crashes on `SELECT CAST(x AS TIMESTAMP) FROM UNNEST([NULL]) AS x`, but not
1497        # on `SELECT CAST(x AS TIMESTAMP) FROM UNNEST([CAST(NULL AS TIMESTAMP)]) AS x`. This
1498        # ensures nulls under the `Values` expression are cast to avoid similar issues.
1499        for value, kind in zip(
1500            values_exp.expressions[0].expressions, source_columns_to_types.values()
1501        ):
1502            if isinstance(value, exp.Null):
1503                value.replace(exp.cast(value, to=kind))
1504
1505    casted_columns = [
1506        exp.alias_(
1507            exp.cast(
1508                exp.column(column) if column in source_columns_to_types else exp.Null(), to=kind
1509            ),
1510            column,
1511            copy=False,
1512        )
1513        for column, kind in target_columns_to_types.items()
1514    ]
1515    return exp.select(*casted_columns).from_(values_exp, copy=False).where(where, copy=False)
1516
1517
1518def pandas_to_sql(
1519    df: pd.DataFrame,
1520    columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None,
1521    batch_size: int = 0,
1522    alias: str = "t",
1523) -> t.Iterator[exp.Select]:
1524    """Convert a pandas dataframe into a VALUES sql statement.
1525
1526    Args:
1527        df: A pandas dataframe to convert.
1528        columns_to_types: Mapping of column names to types to assign to the values.
1529        batch_size: The maximum number of tuples per batches. Defaults to sys.maxsize if <= 0.
1530        alias: The alias to assign to the values expression. If not provided then will default to "t"
1531
1532    Returns:
1533        This method operates as a generator and yields a VALUES expression.
1534    """
1535    yield from select_from_values(
1536        values=list(df.itertuples(index=False, name=None)),
1537        columns_to_types=columns_to_types or columns_to_types_from_df(df),
1538        batch_size=batch_size,
1539        alias=alias,
1540    )
1541
1542
1543def set_default_catalog(
1544    table: str | exp.Table,
1545    default_catalog: t.Optional[str],
1546) -> exp.Table:
1547    table = exp.to_table(table)
1548
1549    if default_catalog and not table.catalog and table.db:
1550        table.set("catalog", exp.parse_identifier(default_catalog))
1551
1552    return table
1553
1554
1555@lru_cache(maxsize=16384)
1556def normalize_model_name(
1557    table: str | exp.Table | exp.Column,
1558    default_catalog: t.Optional[str],
1559    dialect: DialectType = None,
1560) -> str:
1561    if isinstance(table, exp.Column):
1562        table = exp.table_(table.this, db=table.args.get("table"), catalog=table.args.get("db"))
1563    else:
1564        # We are relying on sqlglot's flexible parsing here to accept quotes from other dialects.
1565        # Ex: I have a a normalized name of '"my_table"' but the dialect is spark and therefore we should
1566        # expect spark quotes to be backticks ('`') instead of double quotes ('"'). sqlglot today is flexible
1567        # and will still parse this correctly and we rely on that.
1568        table = exp.to_table(table, dialect=dialect)
1569
1570    table = set_default_catalog(table, default_catalog)
1571    # An alternative way to do this is the following: exp.table_name(table, dialect=dialect, identify=True)
1572    # This though would result in the names being normalized to the target dialect AND the quotes while the below
1573    # approach just normalizes the names.
1574    # By just normalizing names and using sqlglot dialect for quotes this makes it easier for dialects that have
1575    # compatible normalization strategies but incompatible quoting to still work together without user hassle
1576    return exp.table_name(normalize_identifiers(table, dialect=dialect), identify=True)
1577
1578
1579def find_tables(
1580    expression: exp.Expr, default_catalog: t.Optional[str], dialect: DialectType = None
1581) -> t.Set[str]:
1582    """Find all tables referenced in a query.
1583
1584    Caches the result in the meta field 'tables'.
1585
1586    Args:
1587        expressions: The query to find the tables in.
1588        dialect: The dialect to use for normalization of table names.
1589
1590    Returns:
1591        A Set of all the table names.
1592    """
1593    if TABLES_META not in expression.meta:
1594        expression.meta[TABLES_META] = {
1595            normalize_model_name(table, default_catalog=default_catalog, dialect=dialect)
1596            for scope in traverse_scope(expression)
1597            for table in scope.tables
1598            if table.name and table.name not in scope.cte_sources
1599        }
1600    return expression.meta[TABLES_META]
1601
1602
1603def add_table(node: exp.Expr, table: str) -> exp.Expr:
1604    """Add a table to all columns in an expression."""
1605
1606    def _transform(node: exp.Expr) -> exp.Expr:
1607        if isinstance(node, exp.Column) and not node.table:
1608            return exp.column(node.this, table=table)
1609        if isinstance(node, exp.Identifier):
1610            return exp.column(node, table=table)
1611        return node
1612
1613    return node.transform(_transform)
1614
1615
1616def transform_values(
1617    values: t.Tuple[t.Any, ...], columns_to_types: t.Dict[str, exp.DataType]
1618) -> t.Iterator[t.Any]:
1619    """Perform transformations on values given columns_to_types."""
1620
1621    def _transform_value(value: t.Any, dtype: exp.DataType) -> t.Any:
1622        if (
1623            isinstance(value, list)
1624            and dtype.is_type(*exp.DataType.ARRAY_TYPES)
1625            and len(dtype.expressions) == 1
1626        ):
1627            element_type = dtype.expressions[0]
1628            return exp.convert([_transform_value(v, element_type) for v in value])
1629
1630        if (
1631            isinstance(value, dict)
1632            and dtype.is_type(*exp.DataType.STRUCT_TYPES)
1633            and len(value) == len(dtype.expressions)
1634        ):
1635            expressions = []
1636            for (field_name, field_value), field_type in zip(value.items(), dtype.expressions):
1637                if isinstance(field_type, exp.ColumnDef):
1638                    field_type = field_type.kind
1639                else:
1640                    field_type = exp.DataType.build(exp.DataType.Type.UNKNOWN)
1641
1642                expressions.append(
1643                    exp.PropertyEQ(
1644                        this=exp.to_identifier(field_name),
1645                        expression=_transform_value(field_value, field_type),
1646                    )
1647                )
1648
1649            return exp.Struct(expressions=expressions)
1650
1651        if dtype.is_type(exp.DataType.Type.JSON):
1652            return exp.func("PARSE_JSON", f"'{value}'")
1653
1654        return exp.convert(value)
1655
1656    for col_value, col_type in zip(values, columns_to_types.values()):
1657        yield _transform_value(col_value, col_type)
1658
1659
1660def to_schema(sql_path: str | exp.Table, dialect: DialectType = None) -> exp.Table:
1661    if isinstance(sql_path, exp.Table) and sql_path.this is None:
1662        return sql_path
1663    table = exp.to_table(
1664        sql_path.copy() if isinstance(sql_path, exp.Table) else sql_path, dialect=dialect
1665    )
1666    table.set("catalog", table.args.get("db"))
1667    table.set("db", table.args.get("this"))
1668    table.set("this", None)
1669    return table
1670
1671
1672def schema_(
1673    db: exp.Identifier | str,
1674    catalog: t.Optional[exp.Identifier | str] = None,
1675    quoted: t.Optional[bool] = None,
1676) -> exp.Table:
1677    """Build a Schema.
1678
1679    Args:
1680        db: Database name.
1681        catalog: Catalog name.
1682        quoted: Whether to force quotes on the schema's identifiers.
1683
1684    Returns:
1685        The new Schema instance.
1686    """
1687    return exp.Table(
1688        this=None,
1689        db=exp.to_identifier(db, quoted=quoted) if db else None,
1690        catalog=exp.to_identifier(catalog, quoted=quoted) if catalog else None,
1691    )
1692
1693
1694def normalize_mapping_schema(schema: t.Dict, dialect: DialectType) -> MappingSchema:
1695    return MappingSchema(_unquote_schema(schema), dialect=dialect, normalize=False)
1696
1697
1698def _unquote_schema(schema: t.Dict) -> t.Dict:
1699    """SQLGlot schema expects unquoted normalized keys."""
1700    return {
1701        k.strip('"'): _unquote_schema(v) if isinstance(v, dict) else v for k, v in schema.items()
1702    }
1703
1704
1705@contextmanager
1706def normalize_and_quote(
1707    query: E, dialect: DialectType, default_catalog: t.Optional[str], quote: bool = True
1708) -> t.Iterator[E]:
1709    qualify_tables(query, catalog=default_catalog, dialect=dialect)
1710    normalize_identifiers(query, dialect=dialect)
1711    yield query
1712    if quote:
1713        quote_identifiers(query, dialect=dialect)
1714
1715
1716def interpret_expression(e: exp.Expr) -> exp.Expr | str | int | float | bool:
1717    if e.is_int:
1718        return int(e.this)
1719    if e.is_number:
1720        return float(e.this)
1721    if isinstance(e, (exp.Literal, exp.Boolean)):
1722        return e.this
1723    return e
1724
1725
1726def interpret_key_value_pairs(
1727    e: exp.Tuple,
1728) -> t.Dict[str, exp.Expr | str | int | float | bool]:
1729    return {i.this.name: interpret_expression(i.expression) for i in e.expressions}
1730
1731
1732def extract_func_call(
1733    v: exp.Expr, allow_tuples: bool = False
1734) -> t.Tuple[str, t.Dict[str, exp.Expr]]:
1735    kwargs = {}
1736
1737    if isinstance(v, exp.Anonymous):
1738        func = v.name
1739        args = v.expressions
1740    elif isinstance(v, exp.Func):
1741        func = v.sql_name()
1742        args = list(v.args.values())
1743    elif isinstance(v, exp.Paren):
1744        func = ""
1745        args = [v.this]
1746    elif isinstance(v, exp.Tuple):  # airflow only
1747        if not allow_tuples:
1748            raise ConfigError("Audit name is missing (eg. MY_AUDIT())")
1749
1750        func = ""
1751        args = v.expressions
1752    else:
1753        return v.name.lower(), {}
1754
1755    for arg in args:
1756        if not isinstance(arg, (exp.PropertyEQ, exp.EQ)):
1757            raise ConfigError(
1758                f"Function '{func}' must be called with key-value arguments like {func}(arg := value)."
1759            )
1760        kwargs[arg.left.name.lower()] = arg.right
1761    return func.lower(), kwargs
1762
1763
1764def extract_function_calls(func_calls: t.Any, allow_tuples: bool = False) -> t.Any:
1765    """Used for extracting function calls for signals or audits."""
1766
1767    if isinstance(func_calls, (exp.Tuple, exp.Array)):
1768        return [extract_func_call(i, allow_tuples=allow_tuples) for i in func_calls.expressions]
1769    if isinstance(func_calls, exp.Paren):
1770        return [extract_func_call(func_calls.this, allow_tuples=allow_tuples)]
1771    if isinstance(func_calls, exp.Expr):
1772        return [extract_func_call(func_calls, allow_tuples=allow_tuples)]
1773    if isinstance(func_calls, list):
1774        function_calls = []
1775        for entry in func_calls:
1776            if isinstance(entry, dict):
1777                args = entry
1778                name = "" if allow_tuples else entry.pop("name")
1779            elif isinstance(entry, (tuple, list)):
1780                name, args = entry
1781            else:
1782                raise ConfigError(f"Audit must be a dictionary or named tuple. Got {entry}.")
1783
1784            function_calls.append(
1785                (
1786                    name.lower(),
1787                    {
1788                        key: parse_one(value) if isinstance(value, str) else value
1789                        for key, value in args.items()
1790                    },
1791                )
1792            )
1793
1794        return function_calls
1795
1796    return func_calls or []
1797
1798
1799def is_meta_expression(v: t.Any) -> bool:
1800    return isinstance(v, (Audit, Metric, Model))
1801
1802
1803def replace_merge_table_aliases(expression: exp.Expr, dialect: t.Optional[str] = None) -> exp.Expr:
1804    """
1805    Resolves references from the "source" and "target" tables (or their DBT equivalents)
1806    with the corresponding SQLMesh merge aliases (MERGE_SOURCE_ALIAS and MERGE_TARGET_ALIAS)
1807    """
1808    from sqlmesh.core.engine_adapter.base import MERGE_SOURCE_ALIAS, MERGE_TARGET_ALIAS
1809
1810    if isinstance(expression, exp.Column) and (first_part := expression.parts[0]):
1811        if first_part.this.lower() in ("target", "dbt_internal_dest", "__merge_target__"):
1812            first_part.replace(exp.to_identifier(MERGE_TARGET_ALIAS, quoted=True))
1813        elif first_part.this.lower() in ("source", "dbt_internal_source", "__merge_source__"):
1814            first_part.replace(exp.to_identifier(MERGE_SOURCE_ALIAS, quoted=True))
1815
1816    return expression
SQLMESH_MACRO_PREFIX = '@'
TABLES_META = 'sqlmesh.tables'
logger = <Logger sqlmesh.core.dialect (WARNING)>
class Model(sqlglot.expressions.core.Expression):
46class Model(exp.Expression):
47    arg_types = {"expressions": True}
arg_types = {'expressions': True}
key: ClassVar[str] = 'model'
required_args: 't.ClassVar[set[str]]' = {'expressions'}
Inherited Members
sqlglot.expressions.core.Expr
Expr
is_var_len_args
is_subquery
is_cast
is_primitive
dump
load
pipe
apply
sqlglot.expressions.core.Expression
this
expression
expressions
text
is_string
is_number
to_py
is_int
is_star
alias
alias_column_names
name
alias_or_name
output_name
type
is_type
is_leaf
meta
copy
add_comments
pop_comments
append
set
set_kwargs
depth
iter_expressions
find
find_all
find_ancestor
parent_select
same_parent
root
walk
dfs
bfs
unnest
unalias
unnest_operands
flatten
to_s
sql
transform
replace
pop
assert_is
error_messages
and_
or_
not_
update_positions
as_
isin
between
is_
like
ilike
eq
neq
rlike
div
asc
desc
args
parent
arg_key
index
comments
class Audit(sqlglot.expressions.core.Expression):
50class Audit(exp.Expression):
51    arg_types = {"expressions": True}
arg_types = {'expressions': True}
key: ClassVar[str] = 'audit'
required_args: 't.ClassVar[set[str]]' = {'expressions'}
Inherited Members
sqlglot.expressions.core.Expr
Expr
is_var_len_args
is_subquery
is_cast
is_primitive
dump
load
pipe
apply
sqlglot.expressions.core.Expression
this
expression
expressions
text
is_string
is_number
to_py
is_int
is_star
alias
alias_column_names
name
alias_or_name
output_name
type
is_type
is_leaf
meta
copy
add_comments
pop_comments
append
set
set_kwargs
depth
iter_expressions
find
find_all
find_ancestor
parent_select
same_parent
root
walk
dfs
bfs
unnest
unalias
unnest_operands
flatten
to_s
sql
transform
replace
pop
assert_is
error_messages
and_
or_
not_
update_positions
as_
isin
between
is_
like
ilike
eq
neq
rlike
div
asc
desc
args
parent
arg_key
index
comments
class Metric(sqlglot.expressions.core.Expression):
54class Metric(exp.Expression):
55    arg_types = {"expressions": True}
arg_types = {'expressions': True}
key: ClassVar[str] = 'metric'
required_args: 't.ClassVar[set[str]]' = {'expressions'}
Inherited Members
sqlglot.expressions.core.Expr
Expr
is_var_len_args
is_subquery
is_cast
is_primitive
dump
load
pipe
apply
sqlglot.expressions.core.Expression
this
expression
expressions
text
is_string
is_number
to_py
is_int
is_star
alias
alias_column_names
name
alias_or_name
output_name
type
is_type
is_leaf
meta
copy
add_comments
pop_comments
append
set
set_kwargs
depth
iter_expressions
find
find_all
find_ancestor
parent_select
same_parent
root
walk
dfs
bfs
unnest
unalias
unnest_operands
flatten
to_s
sql
transform
replace
pop
assert_is
error_messages
and_
or_
not_
update_positions
as_
isin
between
is_
like
ilike
eq
neq
rlike
div
asc
desc
args
parent
arg_key
index
comments
class Jinja(sqlglot.expressions.core.Expression, sqlglot.expressions.core.Func):
58class Jinja(exp.Expression, exp.Func):
59    arg_types = {"this": True}
arg_types = {'this': True}
key: ClassVar[str] = 'jinja'
required_args: 't.ClassVar[set[str]]' = {'this'}
Inherited Members
sqlglot.expressions.core.Expr
Expr
is_subquery
is_cast
is_primitive
dump
load
pipe
apply
sqlglot.expressions.core.Expression
this
expression
expressions
text
is_string
is_number
to_py
is_int
is_star
alias
alias_column_names
name
alias_or_name
output_name
type
is_type
is_leaf
meta
copy
add_comments
pop_comments
append
set
set_kwargs
depth
iter_expressions
find
find_all
find_ancestor
parent_select
same_parent
root
walk
dfs
bfs
unnest
unalias
unnest_operands
flatten
to_s
sql
transform
replace
pop
assert_is
error_messages
and_
or_
not_
update_positions
as_
isin
between
is_
like
ilike
eq
neq
rlike
div
asc
desc
args
parent
arg_key
index
comments
sqlglot.expressions.core.Func
is_var_len_args
from_arg_list
sql_names
sql_name
default_parser_mappings
class JinjaQuery(Jinja):
62class JinjaQuery(Jinja):
63    pass
key: ClassVar[str] = 'jinjaquery'
required_args: 't.ClassVar[set[str]]' = {'this'}
Inherited Members
sqlglot.expressions.core.Expr
Expr
is_subquery
is_cast
is_primitive
dump
load
pipe
apply
Jinja
arg_types
sqlglot.expressions.core.Expression
this
expression
expressions
text
is_string
is_number
to_py
is_int
is_star
alias
alias_column_names
name
alias_or_name
output_name
type
is_type
is_leaf
meta
copy
add_comments
pop_comments
append
set
set_kwargs
depth
iter_expressions
find
find_all
find_ancestor
parent_select
same_parent
root
walk
dfs
bfs
unnest
unalias
unnest_operands
flatten
to_s
sql
transform
replace
pop
assert_is
error_messages
and_
or_
not_
update_positions
as_
isin
between
is_
like
ilike
eq
neq
rlike
div
asc
desc
args
parent
arg_key
index
comments
sqlglot.expressions.core.Func
is_var_len_args
from_arg_list
sql_names
sql_name
default_parser_mappings
class JinjaStatement(Jinja):
66class JinjaStatement(Jinja):
67    pass
key: ClassVar[str] = 'jinjastatement'
required_args: 't.ClassVar[set[str]]' = {'this'}
Inherited Members
sqlglot.expressions.core.Expr
Expr
is_subquery
is_cast
is_primitive
dump
load
pipe
apply
Jinja
arg_types
sqlglot.expressions.core.Expression
this
expression
expressions
text
is_string
is_number
to_py
is_int
is_star
alias
alias_column_names
name
alias_or_name
output_name
type
is_type
is_leaf
meta
copy
add_comments
pop_comments
append
set
set_kwargs
depth
iter_expressions
find
find_all
find_ancestor
parent_select
same_parent
root
walk
dfs
bfs
unnest
unalias
unnest_operands
flatten
to_s
sql
transform
replace
pop
assert_is
error_messages
and_
or_
not_
update_positions
as_
isin
between
is_
like
ilike
eq
neq
rlike
div
asc
desc
args
parent
arg_key
index
comments
sqlglot.expressions.core.Func
is_var_len_args
from_arg_list
sql_names
sql_name
default_parser_mappings
class VirtualUpdateStatement(sqlglot.expressions.core.Expression):
70class VirtualUpdateStatement(exp.Expression):
71    arg_types = {"expressions": True}
arg_types = {'expressions': True}
key: ClassVar[str] = 'virtualupdatestatement'
required_args: 't.ClassVar[set[str]]' = {'expressions'}
Inherited Members
sqlglot.expressions.core.Expr
Expr
is_var_len_args
is_subquery
is_cast
is_primitive
dump
load
pipe
apply
sqlglot.expressions.core.Expression
this
expression
expressions
text
is_string
is_number
to_py
is_int
is_star
alias
alias_column_names
name
alias_or_name
output_name
type
is_type
is_leaf
meta
copy
add_comments
pop_comments
append
set
set_kwargs
depth
iter_expressions
find
find_all
find_ancestor
parent_select
same_parent
root
walk
dfs
bfs
unnest
unalias
unnest_operands
flatten
to_s
sql
transform
replace
pop
assert_is
error_messages
and_
or_
not_
update_positions
as_
isin
between
is_
like
ilike
eq
neq
rlike
div
asc
desc
args
parent
arg_key
index
comments
class ModelKind(sqlglot.expressions.core.Expression):
74class ModelKind(exp.Expression):
75    arg_types = {"this": True, "expressions": False}
arg_types = {'this': True, 'expressions': False}
key: ClassVar[str] = 'modelkind'
required_args: 't.ClassVar[set[str]]' = {'this'}
Inherited Members
sqlglot.expressions.core.Expr
Expr
is_var_len_args
is_subquery
is_cast
is_primitive
dump
load
pipe
apply
sqlglot.expressions.core.Expression
this
expression
expressions
text
is_string
is_number
to_py
is_int
is_star
alias
alias_column_names
name
alias_or_name
output_name
type
is_type
is_leaf
meta
copy
add_comments
pop_comments
append
set
set_kwargs
depth
iter_expressions
find
find_all
find_ancestor
parent_select
same_parent
root
walk
dfs
bfs
unnest
unalias
unnest_operands
flatten
to_s
sql
transform
replace
pop
assert_is
error_messages
and_
or_
not_
update_positions
as_
isin
between
is_
like
ilike
eq
neq
rlike
div
asc
desc
args
parent
arg_key
index
comments
class MacroVar(sqlglot.expressions.core.Var):
78class MacroVar(exp.Var):
79    pass
key: ClassVar[str] = 'macrovar'
required_args: 't.ClassVar[set[str]]' = {'this'}
Inherited Members
sqlglot.expressions.core.Expr
Expr
arg_types
is_var_len_args
is_subquery
is_cast
dump
load
pipe
apply
sqlglot.expressions.core.Var
is_primitive
sqlglot.expressions.core.Expression
this
expression
expressions
text
is_string
is_number
to_py
is_int
is_star
alias
alias_column_names
name
alias_or_name
output_name
type
is_type
is_leaf
meta
copy
add_comments
pop_comments
append
set
set_kwargs
depth
iter_expressions
find
find_all
find_ancestor
parent_select
same_parent
root
walk
dfs
bfs
unnest
unalias
unnest_operands
flatten
to_s
sql
transform
replace
pop
assert_is
error_messages
and_
or_
not_
update_positions
as_
isin
between
is_
like
ilike
eq
neq
rlike
div
asc
desc
args
parent
arg_key
index
comments
class MacroFunc(sqlglot.expressions.core.Expression, sqlglot.expressions.core.Func):
82class MacroFunc(exp.Expression, exp.Func):
83    @property
84    def name(self) -> str:
85        return self.this.name
name: str
83    @property
84    def name(self) -> str:
85        return self.this.name
key: ClassVar[str] = 'macrofunc'
required_args: 't.ClassVar[set[str]]' = {'this'}
Inherited Members
sqlglot.expressions.core.Expr
Expr
arg_types
is_subquery
is_cast
is_primitive
dump
load
pipe
apply
sqlglot.expressions.core.Expression
this
expression
expressions
text
is_string
is_number
to_py
is_int
is_star
alias
alias_column_names
alias_or_name
output_name
type
is_type
is_leaf
meta
copy
add_comments
pop_comments
append
set
set_kwargs
depth
iter_expressions
find
find_all
find_ancestor
parent_select
same_parent
root
walk
dfs
bfs
unnest
unalias
unnest_operands
flatten
to_s
sql
transform
replace
pop
assert_is
error_messages
and_
or_
not_
update_positions
as_
isin
between
is_
like
ilike
eq
neq
rlike
div
asc
desc
args
parent
arg_key
index
comments
sqlglot.expressions.core.Func
is_var_len_args
from_arg_list
sql_names
sql_name
default_parser_mappings
class MacroDef(MacroFunc):
88class MacroDef(MacroFunc):
89    arg_types = {"this": True, "expression": True}
arg_types = {'this': True, 'expression': True}
key: ClassVar[str] = 'macrodef'
required_args: 't.ClassVar[set[str]]' = {'expression', 'this'}
Inherited Members
sqlglot.expressions.core.Expr
Expr
is_subquery
is_cast
is_primitive
dump
load
pipe
apply
MacroFunc
name
sqlglot.expressions.core.Expression
this
expression
expressions
text
is_string
is_number
to_py
is_int
is_star
alias
alias_column_names
alias_or_name
output_name
type
is_type
is_leaf
meta
copy
add_comments
pop_comments
append
set
set_kwargs
depth
iter_expressions
find
find_all
find_ancestor
parent_select
same_parent
root
walk
dfs
bfs
unnest
unalias
unnest_operands
flatten
to_s
sql
transform
replace
pop
assert_is
error_messages
and_
or_
not_
update_positions
as_
isin
between
is_
like
ilike
eq
neq
rlike
div
asc
desc
args
parent
arg_key
index
comments
sqlglot.expressions.core.Func
is_var_len_args
from_arg_list
sql_names
sql_name
default_parser_mappings
class MacroSQL(MacroFunc):
92class MacroSQL(MacroFunc):
93    arg_types = {"this": True, "into": False}
arg_types = {'this': True, 'into': False}
key: ClassVar[str] = 'macrosql'
required_args: 't.ClassVar[set[str]]' = {'this'}
Inherited Members
sqlglot.expressions.core.Expr
Expr
is_subquery
is_cast
is_primitive
dump
load
pipe
apply
MacroFunc
name
sqlglot.expressions.core.Expression
this
expression
expressions
text
is_string
is_number
to_py
is_int
is_star
alias
alias_column_names
alias_or_name
output_name
type
is_type
is_leaf
meta
copy
add_comments
pop_comments
append
set
set_kwargs
depth
iter_expressions
find
find_all
find_ancestor
parent_select
same_parent
root
walk
dfs
bfs
unnest
unalias
unnest_operands
flatten
to_s
sql
transform
replace
pop
assert_is
error_messages
and_
or_
not_
update_positions
as_
isin
between
is_
like
ilike
eq
neq
rlike
div
asc
desc
args
parent
arg_key
index
comments
sqlglot.expressions.core.Func
is_var_len_args
from_arg_list
sql_names
sql_name
default_parser_mappings
class MacroStrReplace(MacroFunc):
96class MacroStrReplace(MacroFunc):
97    pass
key: ClassVar[str] = 'macrostrreplace'
required_args: 't.ClassVar[set[str]]' = {'this'}
Inherited Members
sqlglot.expressions.core.Expr
Expr
arg_types
is_subquery
is_cast
is_primitive
dump
load
pipe
apply
MacroFunc
name
sqlglot.expressions.core.Expression
this
expression
expressions
text
is_string
is_number
to_py
is_int
is_star
alias
alias_column_names
alias_or_name
output_name
type
is_type
is_leaf
meta
copy
add_comments
pop_comments
append
set
set_kwargs
depth
iter_expressions
find
find_all
find_ancestor
parent_select
same_parent
root
walk
dfs
bfs
unnest
unalias
unnest_operands
flatten
to_s
sql
transform
replace
pop
assert_is
error_messages
and_
or_
not_
update_positions
as_
isin
between
is_
like
ilike
eq
neq
rlike
div
asc
desc
args
parent
arg_key
index
comments
sqlglot.expressions.core.Func
is_var_len_args
from_arg_list
sql_names
sql_name
default_parser_mappings
class PythonCode(sqlglot.expressions.core.Expression):
100class PythonCode(exp.Expression):
101    arg_types = {"expressions": True}
arg_types = {'expressions': True}
key: ClassVar[str] = 'pythoncode'
required_args: 't.ClassVar[set[str]]' = {'expressions'}
Inherited Members
sqlglot.expressions.core.Expr
Expr
is_var_len_args
is_subquery
is_cast
is_primitive
dump
load
pipe
apply
sqlglot.expressions.core.Expression
this
expression
expressions
text
is_string
is_number
to_py
is_int
is_star
alias
alias_column_names
name
alias_or_name
output_name
type
is_type
is_leaf
meta
copy
add_comments
pop_comments
append
set
set_kwargs
depth
iter_expressions
find
find_all
find_ancestor
parent_select
same_parent
root
walk
dfs
bfs
unnest
unalias
unnest_operands
flatten
to_s
sql
transform
replace
pop
assert_is
error_messages
and_
or_
not_
update_positions
as_
isin
between
is_
like
ilike
eq
neq
rlike
div
asc
desc
args
parent
arg_key
index
comments
class DColonCast(sqlglot.expressions.functions.Cast):
104class DColonCast(exp.Cast):
105    pass
key: ClassVar[str] = 'dcoloncast'
required_args: 't.ClassVar[set[str]]' = {'to', 'this'}
Inherited Members
sqlglot.expressions.core.Expr
Expr
is_subquery
is_primitive
dump
load
pipe
apply
sqlglot.expressions.functions.Cast
is_cast
arg_types
name
to
output_name
is_type
sqlglot.expressions.core.Expression
this
expression
expressions
text
is_string
is_number
to_py
is_int
is_star
alias
alias_column_names
alias_or_name
type
is_leaf
meta
copy
add_comments
pop_comments
append
set
set_kwargs
depth
iter_expressions
find
find_all
find_ancestor
parent_select
same_parent
root
walk
dfs
bfs
unnest
unalias
unnest_operands
flatten
to_s
sql
transform
replace
pop
assert_is
error_messages
and_
or_
not_
update_positions
as_
isin
between
is_
like
ilike
eq
neq
rlike
div
asc
desc
args
parent
arg_key
index
comments
sqlglot.expressions.core.Func
is_var_len_args
from_arg_list
sql_names
sql_name
default_parser_mappings
class MetricAgg(sqlglot.expressions.core.Expression, sqlglot.expressions.core.AggFunc):
108class MetricAgg(exp.Expression, exp.AggFunc):
109    """Used for computing metrics."""
110
111    arg_types = {"this": True}
112
113    @property
114    def output_name(self) -> str:
115        return self.this.name

Used for computing metrics.

arg_types = {'this': True}
output_name: str
113    @property
114    def output_name(self) -> str:
115        return self.this.name

Name of the output column if this expression is a selection.

If the Expr has no output name, an empty string is returned.

Example:
>>> from sqlglot import parse_one
>>> parse_one("SELECT a").expressions[0].output_name
'a'
>>> parse_one("SELECT b AS c").expressions[0].output_name
'c'
>>> parse_one("SELECT 1 + 2").expressions[0].output_name
''
key: ClassVar[str] = 'metricagg'
required_args: 't.ClassVar[set[str]]' = {'this'}
Inherited Members
sqlglot.expressions.core.Expr
Expr
is_subquery
is_cast
is_primitive
dump
load
pipe
apply
sqlglot.expressions.core.Expression
this
expression
expressions
text
is_string
is_number
to_py
is_int
is_star
alias
alias_column_names
name
alias_or_name
type
is_type
is_leaf
meta
copy
add_comments
pop_comments
append
set
set_kwargs
depth
iter_expressions
find
find_all
find_ancestor
parent_select
same_parent
root
walk
dfs
bfs
unnest
unalias
unnest_operands
flatten
to_s
sql
transform
replace
pop
assert_is
error_messages
and_
or_
not_
update_positions
as_
isin
between
is_
like
ilike
eq
neq
rlike
div
asc
desc
args
parent
arg_key
index
comments
sqlglot.expressions.core.Func
is_var_len_args
from_arg_list
sql_names
sql_name
default_parser_mappings
class StagedFilePath(sqlglot.expressions.core.Expression):
118class StagedFilePath(exp.Expression):
119    """Represents paths to "staged files" in Snowflake."""
120
121    arg_types = exp.Table.arg_types.copy()

Represents paths to "staged files" in Snowflake.

arg_types = {'this': False, 'alias': False, 'db': False, 'catalog': False, 'laterals': False, 'joins': False, 'pivots': False, 'hints': False, 'system_time': False, 'version': False, 'format': False, 'pattern': False, 'ordinality': False, 'when': False, 'only': False, 'partition': False, 'changes': False, 'rows_from': False, 'sample': False, 'indexed': False}
key: ClassVar[str] = 'stagedfilepath'
required_args: 't.ClassVar[set[str]]' = set()
Inherited Members
sqlglot.expressions.core.Expr
Expr
is_var_len_args
is_subquery
is_cast
is_primitive
dump
load
pipe
apply
sqlglot.expressions.core.Expression
this
expression
expressions
text
is_string
is_number
to_py
is_int
is_star
alias
alias_column_names
name
alias_or_name
output_name
type
is_type
is_leaf
meta
copy
add_comments
pop_comments
append
set
set_kwargs
depth
iter_expressions
find
find_all
find_ancestor
parent_select
same_parent
root
walk
dfs
bfs
unnest
unalias
unnest_operands
flatten
to_s
sql
transform
replace
pop
assert_is
error_messages
and_
or_
not_
update_positions
as_
isin
between
is_
like
ilike
eq
neq
rlike
div
asc
desc
args
parent
arg_key
index
comments
KEYWORD_MACROS = {'WITH', 'LIMIT', 'WHERE', 'GROUP_BY', 'HAVING', 'ORDER_BY', 'JOIN'}
def altercolumn_sql( self: sqlglot.generator.Generator, expression: sqlglot.expressions.ddl.AlterColumn) -> str:
573def altercolumn_sql(self: Generator, expression: exp.AlterColumn) -> str:
574    sql = self._altercolumn_sql(expression)  # type: ignore
575
576    # sqlglot's generator returns as soon as it renders the type, so the nullability parsed
577    # above has to be appended here
578    allow_null = expression.args.get("allow_null")
579    if expression.args.get("dtype") and allow_null is not None:
580        sql = f"{sql} NULL" if allow_null else f"{sql} NOT NULL"
581
582    return sql
PARSERS = {'MODEL': <function _create_parser.<locals>.parse>, 'AUDIT': <function _create_parser.<locals>.parse>, 'METRIC': <function _create_parser.<locals>.parse>}
def format_model_expressions( expressions: List[sqlglot.expressions.core.Expr], dialect: Optional[str] = None, rewrite_casts: bool = True, normalize_functions: Union[str, bool, NoneType] = False, **kwargs: Any) -> str:
1007def format_model_expressions(
1008    expressions: t.List[exp.Expr],
1009    dialect: t.Optional[str] = None,
1010    rewrite_casts: bool = True,
1011    normalize_functions: t.Union[str, bool, None] = False,
1012    **kwargs: t.Any,
1013) -> str:
1014    """Format a model's expressions into a standardized format.
1015
1016    Args:
1017        expressions: The model's expressions, must be at least model def + query.
1018        dialect: The dialect to render the expressions as.
1019        rewrite_casts: Whether to rewrite all casts to use the :: syntax.
1020        normalize_functions: How to normalize function name casing.
1021
1022            * ``False`` (default) — preserves the original spelling of custom and audit
1023              function names.  SQLGlot built-in functions may still canonicalize because
1024              the parser discards the original token.
1025            * ``"upper"`` — uppercases all function names including custom audit
1026              references.
1027            * ``"lower"`` — lowercases all function names including built-ins.
1028            * ``True`` — defers to SQLGlot's generator default (uppercase).
1029            * ``None`` — passes ``None`` directly to the SQLGlot generator, which
1030              defers to SQLGlot's own default (typically uppercase, but may vary by
1031              dialect).  Note: this is the **direct generator API** behaviour.  When
1032              called via ``FormatConfig``, ``None`` is excluded by Pydantic's
1033              ``exclude_none`` serialization and this function receives its own ``False``
1034              default instead — so the two paths are not equivalent.
1035        **kwargs: Additional keyword arguments to pass to the sql generator.
1036
1037    Returns:
1038        A string representing the formatted model.
1039    """
1040
1041    def tag_meta_dialect(expression: exp.Expr) -> exp.Expr:
1042        """Record the model dialect on meta nodes so `_props_sql` can render the
1043        warehouse-SQL properties (columns, audits, physical_properties, ...) with it
1044        while the SQLMesh-owned ones stay dialect-agnostic. Tags nested ModelKind
1045        nodes too, since kinds carry expression properties of their own such as
1046        `time_data_type` and `unique_key`."""
1047        if not dialect or not is_meta_expression(expression):
1048            return expression
1049
1050        expression = expression.copy()
1051        for node in expression.find_all(Model, Audit, Metric, ModelKind):
1052            node.meta[_SQLMESH_META_DIALECT] = dialect
1053        expression.meta[_SQLMESH_META_DIALECT] = dialect
1054        return expression
1055
1056    if len(expressions) == 1 and is_meta_expression(expressions[0]):
1057        # Meta expressions (MODEL/AUDIT/METRIC) are SQLMesh DDL, not standard SQL,
1058        # so they must never be transpiled to the target dialect (e.g. tsql would
1059        # rewrite a boolean property like `allow_partials TRUE` to `(1 = 1)`).
1060        # Individual properties whose values *are* warehouse SQL still render with
1061        # the model dialect -- see `_props_sql` / `_meta_render_policy`.
1062        return tag_meta_dialect(expressions[0]).sql(
1063            pretty=True, dialect=None, normalize_functions=normalize_functions
1064        )
1065
1066    if rewrite_casts:
1067
1068        def cast_to_colon(node: exp.Expr) -> exp.Expr:
1069            # Directly check type instead of isinstance to avoid rewriting subclasses of CAST, e.g. JSONCast
1070            if type(node) is exp.Cast and not any(
1071                # Only convert CAST into :: if it doesn't have additional args set, otherwise this
1072                # conversion could alter the semantics (eg. changing SAFE_CAST in BigQuery to CAST)
1073                arg
1074                for name, arg in node.args.items()
1075                if name not in ("this", "to")
1076            ):
1077                this = node.this
1078
1079                if not isinstance(this, (exp.Binary, exp.Unary)) or isinstance(this, exp.Paren):
1080                    cast = DColonCast(this=this, to=node.to)
1081                    cast.comments = node.comments
1082                    node = cast
1083
1084            exp.replace_children(node, cast_to_colon)
1085            return node
1086
1087        new_expressions = []
1088        for expression in expressions:
1089            expression = expression.copy()
1090            exp.replace_children(expression, cast_to_colon)
1091            new_expressions.append(expression)
1092
1093        expressions = new_expressions
1094
1095    return ";\n\n".join(
1096        # Meta expressions (MODEL/AUDIT/METRIC) are SQLMesh DDL and must stay
1097        # dialect-agnostic; only the actual query/statement expressions transpile.
1098        tag_meta_dialect(expression).sql(
1099            pretty=True,
1100            dialect=None if is_meta_expression(expression) else dialect,
1101            normalize_functions=normalize_functions,
1102            **kwargs,
1103        )
1104        for expression in expressions
1105    ).strip()

Format a model's expressions into a standardized format.

Arguments:
  • expressions: The model's expressions, must be at least model def + query.
  • dialect: The dialect to render the expressions as.
  • rewrite_casts: Whether to rewrite all casts to use the :: syntax.
  • normalize_functions: How to normalize function name casing.

    • False (default) — preserves the original spelling of custom and audit function names. SQLGlot built-in functions may still canonicalize because the parser discards the original token.
    • "upper" — uppercases all function names including custom audit references.
    • "lower" — lowercases all function names including built-ins.
    • True — defers to SQLGlot's generator default (uppercase).
    • None — passes None directly to the SQLGlot generator, which defers to SQLGlot's own default (typically uppercase, but may vary by dialect). Note: this is the direct generator API behaviour. When called via FormatConfig, None is excluded by Pydantic's exclude_none serialization and this function receives its own False default instead — so the two paths are not equivalent.
  • **kwargs: Additional keyword arguments to pass to the sql generator.
Returns:

A string representing the formatted model.

def text_diff( a: List[sqlglot.expressions.core.Expr], b: List[sqlglot.expressions.core.Expr], a_dialect: Optional[str] = None, b_dialect: Optional[str] = None) -> str:
1108def text_diff(
1109    a: t.List[exp.Expr],
1110    b: t.List[exp.Expr],
1111    a_dialect: t.Optional[str] = None,
1112    b_dialect: t.Optional[str] = None,
1113) -> str:
1114    """Find the unified text diff between two expressions."""
1115    a_sql = [
1116        line
1117        for expr in a
1118        for line in expr.sql(pretty=True, comments=False, dialect=a_dialect).split("\n")
1119    ]
1120    b_sql = [
1121        line
1122        for expr in b
1123        for line in expr.sql(pretty=True, comments=False, dialect=b_dialect).split("\n")
1124    ]
1125    return "\n".join(unified_diff(a_sql, b_sql))

Find the unified text diff between two expressions.

WS_OR_COMMENT = '(?:\\s|--[^\\n]*\\n|/\\*.*?\\*/)'
KEY_BOUNDARY = '(?:\\(|,)'
DIALECT_VALUE = '[\'\\"]?(?P<dialect>[a-z][a-z0-9]*)[\'\\"]?'
VALUE_BOUNDARY = '(?=,|\\))'
DIALECT_PATTERN = re.compile('\\b(?:model|audit)\\b(?=\\s*\\().*?(?:\\(|,)(?:\\s|--[^\\n]*\\n|/\\*.*?\\*/)*dialect(?:\\s|--[^\\n]*\\n|/\\*.*?\\*/)+[\'\\"]?(?P<dialect>[a-z][a-z0-9]*)[\'\\"]?(?:\\s|--[^\\n]*\\n|/\\*.*?\\*/)*(?=,|\, re.IGNORECASE|re.DOTALL)
JINJA_QUERY_BEGIN = 'JINJA_QUERY_BEGIN'
JINJA_STATEMENT_BEGIN = 'JINJA_STATEMENT_BEGIN'
JINJA_END = 'JINJA_END'
ON_VIRTUAL_UPDATE_BEGIN = 'ON_VIRTUAL_UPDATE_BEGIN'
ON_VIRTUAL_UPDATE_END = 'ON_VIRTUAL_UPDATE_END'
def jinja_query(query: str) -> JinjaQuery:
1169def jinja_query(query: str) -> JinjaQuery:
1170    return JinjaQuery(this=exp.Literal.string(query.strip()))
def jinja_statement(statement: str) -> JinjaStatement:
1173def jinja_statement(statement: str) -> JinjaStatement:
1174    return JinjaStatement(this=exp.Literal.string(statement.strip()))
def virtual_statement( statements: List[sqlglot.expressions.core.Expr]) -> VirtualUpdateStatement:
1185def virtual_statement(statements: t.List[exp.Expr]) -> VirtualUpdateStatement:
1186    return VirtualUpdateStatement(expressions=statements)
class ChunkType(enum.Enum):
1189class ChunkType(Enum):
1190    JINJA_QUERY = auto()
1191    JINJA_STATEMENT = auto()
1192    SQL = auto()
1193    VIRTUAL_STATEMENT = auto()
1194    VIRTUAL_JINJA_STATEMENT = auto()

An enumeration.

JINJA_QUERY = <ChunkType.JINJA_QUERY: 1>
JINJA_STATEMENT = <ChunkType.JINJA_STATEMENT: 2>
SQL = <ChunkType.SQL: 3>
VIRTUAL_STATEMENT = <ChunkType.VIRTUAL_STATEMENT: 4>
VIRTUAL_JINJA_STATEMENT = <ChunkType.VIRTUAL_JINJA_STATEMENT: 5>
Inherited Members
enum.Enum
name
value
def parse_one( sql: str, dialect: Optional[str] = None, into: Union[type[sqlglot.expressions.core.Expr], collections.abc.Collection[type[sqlglot.expressions.core.Expr]], NoneType] = None) -> sqlglot.expressions.core.Expr:
1197def parse_one(
1198    sql: str, dialect: t.Optional[str] = None, into: t.Optional[exp.IntoType] = None
1199) -> exp.Expr:
1200    expressions = parse(sql, default_dialect=dialect, match_dialect=False, into=into)
1201    if not expressions:
1202        raise SQLMeshError(f"No expressions found in '{sql}'")
1203    elif len(expressions) > 1:
1204        raise SQLMeshError(f"Multiple expressions found in '{sql}'")
1205    return expressions[0]
def parse( sql: str, default_dialect: Optional[str] = None, match_dialect: bool = True, into: Union[type[sqlglot.expressions.core.Expr], collections.abc.Collection[type[sqlglot.expressions.core.Expr]], NoneType] = None) -> List[sqlglot.expressions.core.Expr]:
1208def parse(
1209    sql: str,
1210    default_dialect: t.Optional[str] = None,
1211    match_dialect: bool = True,
1212    into: t.Optional[exp.IntoType] = None,
1213) -> t.List[exp.Expr]:
1214    """Parse a sql string.
1215
1216    Supports parsing model definition.
1217    If a jinja block is detected, the query is stored as raw string in a Jinja node.
1218
1219    Args:
1220        sql: The sql based definition.
1221        default_dialect: The dialect to use if the model does not specify one.
1222
1223    Returns:
1224        A list of the parsed expressions: [Model, *Statements, Query, *Statements]
1225    """
1226    match = match_dialect and DIALECT_PATTERN.search(sql[:MAX_MODEL_DEFINITION_SIZE])
1227    dialect_str = match.group("dialect") if match else None
1228    dialect = Dialect.get_or_raise(dialect_str or default_dialect)
1229
1230    tokens = dialect.tokenize(sql)
1231    chunks: t.List[t.Tuple[t.List[Token], ChunkType]] = [([], ChunkType.SQL)]
1232    total = len(tokens)
1233
1234    pos = 0
1235    virtual = False
1236    while pos < total:
1237        token = tokens[pos]
1238        if _is_virtual_statement_end(tokens, pos):
1239            chunks[-1][0].append(token)
1240            virtual = False
1241            chunks.append(([], ChunkType.SQL))
1242            pos += 2
1243        elif _is_jinja_end(tokens, pos) or (
1244            chunks[-1][1] == ChunkType.SQL
1245            and token.token_type == TokenType.SEMICOLON
1246            and pos < total - 1
1247        ):
1248            if token.token_type == TokenType.SEMICOLON:
1249                pos += 1
1250            else:
1251                # Jinja end statement
1252                chunks[-1][0].append(token)
1253                pos += 2
1254            chunks.append(
1255                (
1256                    [],
1257                    ChunkType.VIRTUAL_STATEMENT
1258                    if virtual and tokens[pos] != ON_VIRTUAL_UPDATE_END
1259                    else ChunkType.SQL,
1260                )
1261            )
1262        elif _is_jinja_query_begin(tokens, pos):
1263            chunks.append(([token], ChunkType.JINJA_QUERY))
1264            pos += 2
1265        elif _is_jinja_statement_begin(tokens, pos):
1266            chunks.append(([token], ChunkType.JINJA_STATEMENT))
1267            pos += 2
1268        elif _is_virtual_statement_begin(tokens, pos):
1269            chunks.append(([token], ChunkType.VIRTUAL_STATEMENT))
1270            pos += 2
1271            virtual = True
1272        else:
1273            chunks[-1][0].append(token)
1274            pos += 1
1275
1276    parser = dialect.parser()
1277    expressions: t.List[exp.Expr] = []
1278
1279    def parse_sql_chunk(chunk: t.List[Token], meta_sql: bool = True) -> t.List[exp.Expr]:
1280        parsed_expressions: t.List[t.Optional[exp.Expr]] = (
1281            parser.parse(chunk, sql) if into is None else parser.parse_into(into, chunk, sql)
1282        )
1283        expressions = []
1284        for expression in parsed_expressions:
1285            if expression:
1286                if meta_sql:
1287                    expression.meta["sql"] = parser._find_sql(chunk[0], chunk[-1])
1288                expressions.append(expression)
1289        return expressions
1290
1291    def parse_jinja_chunk(chunk: t.List[Token], meta_sql: bool = True) -> exp.Expr:
1292        start, *_, end = chunk
1293        segment = sql[start.end + 2 : end.start - 1]
1294        factory = jinja_query if chunk_type == ChunkType.JINJA_QUERY else jinja_statement
1295        expression = factory(segment.strip())
1296        if meta_sql:
1297            expression.meta["sql"] = sql[start.start : end.end + 1]
1298        return expression
1299
1300    def parse_virtual_statement(
1301        chunks: t.List[t.Tuple[t.List[Token], ChunkType]], pos: int
1302    ) -> t.Tuple[t.List[exp.Expr], int]:
1303        # For virtual statements we need to handle both SQL and Jinja nested blocks within the chunk
1304        virtual_update_statements: t.List[exp.Expr] = []
1305        start = chunks[pos][0][0].start
1306
1307        while (
1308            chunks[pos - 1][0] == [] or chunks[pos - 1][0][-1].text.upper() != ON_VIRTUAL_UPDATE_END
1309        ):
1310            chunk, chunk_type = chunks[pos]
1311            if chunk_type == ChunkType.JINJA_STATEMENT:
1312                virtual_update_statements.append(parse_jinja_chunk(chunk, False))
1313            else:
1314                virtual_update_statements.extend(
1315                    parse_sql_chunk(
1316                        chunk[int(chunk[0].text.upper() == ON_VIRTUAL_UPDATE_BEGIN) : -1], False
1317                    ),
1318                )
1319            pos += 1
1320
1321        if virtual_update_statements:
1322            statements = virtual_statement(virtual_update_statements)
1323            end = chunk[-1].end + 1
1324            statements.meta["sql"] = sql[start:end]
1325            return [statements], pos
1326
1327        return [], pos
1328
1329    pos = 0
1330    total_chunks = len(chunks)
1331    while pos < total_chunks:
1332        chunk, chunk_type = chunks[pos]
1333        if chunk_type == ChunkType.VIRTUAL_STATEMENT:
1334            virtual_expression, pos = parse_virtual_statement(chunks, pos)
1335            expressions.extend(virtual_expression)
1336        elif chunk_type == ChunkType.SQL:
1337            expressions.extend(parse_sql_chunk(chunk))
1338        else:
1339            expressions.append(parse_jinja_chunk(chunk))
1340        pos += 1
1341
1342    return expressions

Parse a sql string.

Supports parsing model definition. If a jinja block is detected, the query is stored as raw string in a Jinja node.

Arguments:
  • sql: The sql based definition.
  • default_dialect: The dialect to use if the model does not specify one.
Returns:

A list of the parsed expressions: [Model, *Statements, Query, *Statements]

def extend_sqlglot() -> None:
1345def extend_sqlglot() -> None:
1346    """Extend SQLGlot with SQLMesh's custom macro aware dialect."""
1347    tokenizers = {Tokenizer}
1348    parsers = {Parser}
1349    generators = {Generator}
1350
1351    for dialect in Dialect.classes.values():
1352        # Athena picks a different Tokenizer / Parser / Generator depending on the query
1353        # so this ensures that the extra ones it defines are also extended
1354        if dialect == athena.Athena:
1355            tokenizers.add(athena._TrinoTokenizer)
1356            parsers.add(AthenaTrinoParser)
1357            generators.add(athena_generators.AthenaTrinoGenerator)
1358            generators.add(athena_generators._HiveGenerator)
1359
1360        if hasattr(dialect, "Tokenizer"):
1361            tokenizers.add(dialect.Tokenizer)
1362        if hasattr(dialect, "Parser"):
1363            parsers.add(dialect.Parser)
1364        if hasattr(dialect, "Generator"):
1365            generators.add(dialect.Generator)
1366
1367    for tokenizer in tokenizers:
1368        tokenizer.VAR_SINGLE_TOKENS.update(SQLMESH_MACRO_PREFIX)
1369
1370    for parser in parsers:
1371        parser.FUNCTIONS.update({"JINJA": Jinja.from_arg_list, "METRIC": MetricAgg.from_arg_list})
1372        parser.PLACEHOLDER_PARSERS.update({TokenType.PARAMETER: _parse_macro})
1373        parser.QUERY_MODIFIER_PARSERS.update(
1374            {TokenType.PARAMETER: lambda self: _parse_body_macro(self)}
1375        )
1376
1377    for generator in generators:
1378        if MacroFunc not in generator.TRANSFORMS:
1379            generator.TRANSFORMS.update(
1380                {
1381                    Audit: lambda self, e: _sqlmesh_ddl_sql(self, e, "AUDIT"),
1382                    DColonCast: lambda self, e: f"{self.sql(e, 'this')}::{self.sql(e, 'to')}",
1383                    Jinja: lambda self, e: e.name,
1384                    JinjaQuery: lambda self, e: f"{JINJA_QUERY_BEGIN};\n{e.name}\n{JINJA_END};",
1385                    JinjaStatement: lambda self, e: (
1386                        f"{JINJA_STATEMENT_BEGIN};\n{e.name}\n{JINJA_END};"
1387                    ),
1388                    VirtualUpdateStatement: lambda self, e: _on_virtual_update_sql(self, e),
1389                    MacroDef: lambda self, e: f"@DEF({self.sql(e.this)}, {self.sql(e.expression)})",
1390                    MacroFunc: _macro_func_sql,
1391                    MacroStrReplace: lambda self, e: f"@{self.sql(e.this)}",
1392                    MacroSQL: lambda self, e: f"@SQL({self.sql(e.this)})",
1393                    MacroVar: lambda self, e: f"@{e.name}",
1394                    Metric: lambda self, e: _sqlmesh_ddl_sql(self, e, "METRIC"),
1395                    Model: lambda self, e: _sqlmesh_ddl_sql(self, e, "MODEL"),
1396                    ModelKind: _model_kind_sql,
1397                    PythonCode: lambda self, e: self.expressions(e, sep="\n", indent=False),
1398                    StagedFilePath: lambda self, e: self.table_sql(e),
1399                    exp.Whens: _whens_sql,
1400                }
1401            )
1402        if MacroDef not in generator.WITH_SEPARATED_COMMENTS:
1403            generator.WITH_SEPARATED_COMMENTS = (
1404                *generator.WITH_SEPARATED_COMMENTS,
1405                Model,
1406                MacroDef,
1407            )
1408
1409        if MacroVar not in generator.UNWRAPPED_INTERVAL_VALUES:
1410            generator.UNWRAPPED_INTERVAL_VALUES = (
1411                *generator.UNWRAPPED_INTERVAL_VALUES,
1412                MacroStrReplace,
1413                MacroVar,
1414            )
1415
1416    _override(Parser, _parse_select)
1417    _override(Parser, _parse_statement)
1418    _override(Parser, _parse_join)
1419    _override(Parser, _parse_order)
1420    _override(Parser, _parse_where)
1421    _override(Parser, _parse_group)
1422    _override(Parser, _parse_with)
1423    _override(Parser, _parse_having)
1424    _override(Parser, _parse_limit)
1425    _override(Parser, _parse_value)
1426    _override(Parser, _parse_lambda)
1427    _override(Parser, _parse_types)
1428    _override(Parser, _parse_if)
1429    _override(TSQL.Parser, Parser._parse_if)
1430    _override(Parser, _parse_id_var)
1431    _override(Parser, _parse_interval_span)
1432    _override(Parser, _warn_unsupported)
1433    _override(Snowflake.Parser, _parse_table_parts)
1434    _override(TSQL.Parser, _parse_alter_table_alter)
1435    _override(TSQL.Generator, altercolumn_sql)
1436
1437    # DuckDB's prefix absolute power operator `@` clashes with the macro syntax
1438    DuckDB.Parser.NO_PAREN_FUNCTION_PARSERS.pop("@", None)

Extend SQLGlot with SQLMesh's custom macro aware dialect.

def select_from_values( values: List[Tuple[Any, ...]], columns_to_types: Dict[str, sqlglot.expressions.datatypes.DataType], batch_size: int = 0, alias: str = 't') -> Iterator[sqlglot.expressions.query.Select]:
1441def select_from_values(
1442    values: t.List[t.Tuple[t.Any, ...]],
1443    columns_to_types: t.Dict[str, exp.DataType],
1444    batch_size: int = 0,
1445    alias: str = "t",
1446) -> t.Iterator[exp.Select]:
1447    """Generate a VALUES expression that has a select wrapped around it to cast the values to their correct types.
1448
1449    Args:
1450        values: List of values to use for the VALUES expression.
1451        columns_to_types: Mapping of column names to types to assign to the values.
1452        batch_size: The maximum number of tuples per batches. Defaults to sys.maxsize if <= 0.
1453        alias: The alias to assign to the values expression. If not provided then will default to "t"
1454
1455    Returns:
1456        This method operates as a generator and yields a VALUES expression.
1457    """
1458    if batch_size <= 0:
1459        batch_size = sys.maxsize
1460    num_rows = len(values)
1461    for i in range(0, num_rows, batch_size):
1462        yield select_from_values_for_batch_range(
1463            values=values,
1464            target_columns_to_types=columns_to_types,
1465            batch_start=i,
1466            batch_end=min(i + batch_size, num_rows),
1467            alias=alias,
1468        )

Generate a VALUES expression that has a select wrapped around it to cast the values to their correct types.

Arguments:
  • values: List of values to use for the VALUES expression.
  • columns_to_types: Mapping of column names to types to assign to the values.
  • batch_size: The maximum number of tuples per batches. Defaults to sys.maxsize if <= 0.
  • alias: The alias to assign to the values expression. If not provided then will default to "t"
Returns:

This method operates as a generator and yields a VALUES expression.

def select_from_values_for_batch_range( values: List[Tuple[Any, ...]], target_columns_to_types: Dict[str, sqlglot.expressions.datatypes.DataType], batch_start: int, batch_end: int, alias: str = 't', source_columns: Optional[List[str]] = None) -> sqlglot.expressions.query.Select:
1471def select_from_values_for_batch_range(
1472    values: t.List[t.Tuple[t.Any, ...]],
1473    target_columns_to_types: t.Dict[str, exp.DataType],
1474    batch_start: int,
1475    batch_end: int,
1476    alias: str = "t",
1477    source_columns: t.Optional[t.List[str]] = None,
1478) -> exp.Select:
1479    source_columns = source_columns or list(target_columns_to_types)
1480    source_columns_to_types = get_source_columns_to_types(target_columns_to_types, source_columns)
1481
1482    if not values:
1483        # Ensures we don't generate an empty VALUES clause & forces a zero-row output
1484        where = exp.false()
1485        expressions = [
1486            tuple(exp.cast(exp.null(), to=kind) for kind in source_columns_to_types.values())
1487        ]
1488    else:
1489        where = None
1490        expressions = [
1491            tuple(transform_values(v, source_columns_to_types))
1492            for v in values[batch_start:batch_end]
1493        ]
1494
1495    values_exp = exp.values(expressions, alias=alias, columns=source_columns_to_types)
1496    if values:
1497        # BigQuery crashes on `SELECT CAST(x AS TIMESTAMP) FROM UNNEST([NULL]) AS x`, but not
1498        # on `SELECT CAST(x AS TIMESTAMP) FROM UNNEST([CAST(NULL AS TIMESTAMP)]) AS x`. This
1499        # ensures nulls under the `Values` expression are cast to avoid similar issues.
1500        for value, kind in zip(
1501            values_exp.expressions[0].expressions, source_columns_to_types.values()
1502        ):
1503            if isinstance(value, exp.Null):
1504                value.replace(exp.cast(value, to=kind))
1505
1506    casted_columns = [
1507        exp.alias_(
1508            exp.cast(
1509                exp.column(column) if column in source_columns_to_types else exp.Null(), to=kind
1510            ),
1511            column,
1512            copy=False,
1513        )
1514        for column, kind in target_columns_to_types.items()
1515    ]
1516    return exp.select(*casted_columns).from_(values_exp, copy=False).where(where, copy=False)
def pandas_to_sql( df: pandas.core.frame.DataFrame, columns_to_types: Optional[Dict[str, sqlglot.expressions.datatypes.DataType]] = None, batch_size: int = 0, alias: str = 't') -> Iterator[sqlglot.expressions.query.Select]:
1519def pandas_to_sql(
1520    df: pd.DataFrame,
1521    columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None,
1522    batch_size: int = 0,
1523    alias: str = "t",
1524) -> t.Iterator[exp.Select]:
1525    """Convert a pandas dataframe into a VALUES sql statement.
1526
1527    Args:
1528        df: A pandas dataframe to convert.
1529        columns_to_types: Mapping of column names to types to assign to the values.
1530        batch_size: The maximum number of tuples per batches. Defaults to sys.maxsize if <= 0.
1531        alias: The alias to assign to the values expression. If not provided then will default to "t"
1532
1533    Returns:
1534        This method operates as a generator and yields a VALUES expression.
1535    """
1536    yield from select_from_values(
1537        values=list(df.itertuples(index=False, name=None)),
1538        columns_to_types=columns_to_types or columns_to_types_from_df(df),
1539        batch_size=batch_size,
1540        alias=alias,
1541    )

Convert a pandas dataframe into a VALUES sql statement.

Arguments:
  • df: A pandas dataframe to convert.
  • columns_to_types: Mapping of column names to types to assign to the values.
  • batch_size: The maximum number of tuples per batches. Defaults to sys.maxsize if <= 0.
  • alias: The alias to assign to the values expression. If not provided then will default to "t"
Returns:

This method operates as a generator and yields a VALUES expression.

def set_default_catalog( table: str | sqlglot.expressions.query.Table, default_catalog: Optional[str]) -> sqlglot.expressions.query.Table:
1544def set_default_catalog(
1545    table: str | exp.Table,
1546    default_catalog: t.Optional[str],
1547) -> exp.Table:
1548    table = exp.to_table(table)
1549
1550    if default_catalog and not table.catalog and table.db:
1551        table.set("catalog", exp.parse_identifier(default_catalog))
1552
1553    return table
@lru_cache(maxsize=16384)
def normalize_model_name( table: str | sqlglot.expressions.query.Table | sqlglot.expressions.core.Column, default_catalog: Optional[str], dialect: Union[str, sqlglot.dialects.dialect.Dialect, type[sqlglot.dialects.dialect.Dialect], NoneType] = None) -> str:
1556@lru_cache(maxsize=16384)
1557def normalize_model_name(
1558    table: str | exp.Table | exp.Column,
1559    default_catalog: t.Optional[str],
1560    dialect: DialectType = None,
1561) -> str:
1562    if isinstance(table, exp.Column):
1563        table = exp.table_(table.this, db=table.args.get("table"), catalog=table.args.get("db"))
1564    else:
1565        # We are relying on sqlglot's flexible parsing here to accept quotes from other dialects.
1566        # Ex: I have a a normalized name of '"my_table"' but the dialect is spark and therefore we should
1567        # expect spark quotes to be backticks ('`') instead of double quotes ('"'). sqlglot today is flexible
1568        # and will still parse this correctly and we rely on that.
1569        table = exp.to_table(table, dialect=dialect)
1570
1571    table = set_default_catalog(table, default_catalog)
1572    # An alternative way to do this is the following: exp.table_name(table, dialect=dialect, identify=True)
1573    # This though would result in the names being normalized to the target dialect AND the quotes while the below
1574    # approach just normalizes the names.
1575    # By just normalizing names and using sqlglot dialect for quotes this makes it easier for dialects that have
1576    # compatible normalization strategies but incompatible quoting to still work together without user hassle
1577    return exp.table_name(normalize_identifiers(table, dialect=dialect), identify=True)
def find_tables( expression: sqlglot.expressions.core.Expr, default_catalog: Optional[str], dialect: Union[str, sqlglot.dialects.dialect.Dialect, type[sqlglot.dialects.dialect.Dialect], NoneType] = None) -> Set[str]:
1580def find_tables(
1581    expression: exp.Expr, default_catalog: t.Optional[str], dialect: DialectType = None
1582) -> t.Set[str]:
1583    """Find all tables referenced in a query.
1584
1585    Caches the result in the meta field 'tables'.
1586
1587    Args:
1588        expressions: The query to find the tables in.
1589        dialect: The dialect to use for normalization of table names.
1590
1591    Returns:
1592        A Set of all the table names.
1593    """
1594    if TABLES_META not in expression.meta:
1595        expression.meta[TABLES_META] = {
1596            normalize_model_name(table, default_catalog=default_catalog, dialect=dialect)
1597            for scope in traverse_scope(expression)
1598            for table in scope.tables
1599            if table.name and table.name not in scope.cte_sources
1600        }
1601    return expression.meta[TABLES_META]

Find all tables referenced in a query.

Caches the result in the meta field 'tables'.

Arguments:
  • expressions: The query to find the tables in.
  • dialect: The dialect to use for normalization of table names.
Returns:

A Set of all the table names.

def add_table( node: sqlglot.expressions.core.Expr, table: str) -> sqlglot.expressions.core.Expr:
1604def add_table(node: exp.Expr, table: str) -> exp.Expr:
1605    """Add a table to all columns in an expression."""
1606
1607    def _transform(node: exp.Expr) -> exp.Expr:
1608        if isinstance(node, exp.Column) and not node.table:
1609            return exp.column(node.this, table=table)
1610        if isinstance(node, exp.Identifier):
1611            return exp.column(node, table=table)
1612        return node
1613
1614    return node.transform(_transform)

Add a table to all columns in an expression.

def transform_values( values: Tuple[Any, ...], columns_to_types: Dict[str, sqlglot.expressions.datatypes.DataType]) -> Iterator[Any]:
1617def transform_values(
1618    values: t.Tuple[t.Any, ...], columns_to_types: t.Dict[str, exp.DataType]
1619) -> t.Iterator[t.Any]:
1620    """Perform transformations on values given columns_to_types."""
1621
1622    def _transform_value(value: t.Any, dtype: exp.DataType) -> t.Any:
1623        if (
1624            isinstance(value, list)
1625            and dtype.is_type(*exp.DataType.ARRAY_TYPES)
1626            and len(dtype.expressions) == 1
1627        ):
1628            element_type = dtype.expressions[0]
1629            return exp.convert([_transform_value(v, element_type) for v in value])
1630
1631        if (
1632            isinstance(value, dict)
1633            and dtype.is_type(*exp.DataType.STRUCT_TYPES)
1634            and len(value) == len(dtype.expressions)
1635        ):
1636            expressions = []
1637            for (field_name, field_value), field_type in zip(value.items(), dtype.expressions):
1638                if isinstance(field_type, exp.ColumnDef):
1639                    field_type = field_type.kind
1640                else:
1641                    field_type = exp.DataType.build(exp.DataType.Type.UNKNOWN)
1642
1643                expressions.append(
1644                    exp.PropertyEQ(
1645                        this=exp.to_identifier(field_name),
1646                        expression=_transform_value(field_value, field_type),
1647                    )
1648                )
1649
1650            return exp.Struct(expressions=expressions)
1651
1652        if dtype.is_type(exp.DataType.Type.JSON):
1653            return exp.func("PARSE_JSON", f"'{value}'")
1654
1655        return exp.convert(value)
1656
1657    for col_value, col_type in zip(values, columns_to_types.values()):
1658        yield _transform_value(col_value, col_type)

Perform transformations on values given columns_to_types.

def to_schema( sql_path: str | sqlglot.expressions.query.Table, dialect: Union[str, sqlglot.dialects.dialect.Dialect, type[sqlglot.dialects.dialect.Dialect], NoneType] = None) -> sqlglot.expressions.query.Table:
1661def to_schema(sql_path: str | exp.Table, dialect: DialectType = None) -> exp.Table:
1662    if isinstance(sql_path, exp.Table) and sql_path.this is None:
1663        return sql_path
1664    table = exp.to_table(
1665        sql_path.copy() if isinstance(sql_path, exp.Table) else sql_path, dialect=dialect
1666    )
1667    table.set("catalog", table.args.get("db"))
1668    table.set("db", table.args.get("this"))
1669    table.set("this", None)
1670    return table
def schema_( db: sqlglot.expressions.core.Identifier | str, catalog: Union[sqlglot.expressions.core.Identifier, str, NoneType] = None, quoted: Optional[bool] = None) -> sqlglot.expressions.query.Table:
1673def schema_(
1674    db: exp.Identifier | str,
1675    catalog: t.Optional[exp.Identifier | str] = None,
1676    quoted: t.Optional[bool] = None,
1677) -> exp.Table:
1678    """Build a Schema.
1679
1680    Args:
1681        db: Database name.
1682        catalog: Catalog name.
1683        quoted: Whether to force quotes on the schema's identifiers.
1684
1685    Returns:
1686        The new Schema instance.
1687    """
1688    return exp.Table(
1689        this=None,
1690        db=exp.to_identifier(db, quoted=quoted) if db else None,
1691        catalog=exp.to_identifier(catalog, quoted=quoted) if catalog else None,
1692    )

Build a Schema.

Arguments:
  • db: Database name.
  • catalog: Catalog name.
  • quoted: Whether to force quotes on the schema's identifiers.
Returns:

The new Schema instance.

def normalize_mapping_schema( schema: Dict, dialect: Union[str, sqlglot.dialects.dialect.Dialect, type[sqlglot.dialects.dialect.Dialect], NoneType]) -> sqlglot.schema.MappingSchema:
1695def normalize_mapping_schema(schema: t.Dict, dialect: DialectType) -> MappingSchema:
1696    return MappingSchema(_unquote_schema(schema), dialect=dialect, normalize=False)
@contextmanager
def normalize_and_quote( query: ~E, dialect: Union[str, sqlglot.dialects.dialect.Dialect, type[sqlglot.dialects.dialect.Dialect], NoneType], default_catalog: Optional[str], quote: bool = True) -> Iterator[~E]:
1706@contextmanager
1707def normalize_and_quote(
1708    query: E, dialect: DialectType, default_catalog: t.Optional[str], quote: bool = True
1709) -> t.Iterator[E]:
1710    qualify_tables(query, catalog=default_catalog, dialect=dialect)
1711    normalize_identifiers(query, dialect=dialect)
1712    yield query
1713    if quote:
1714        quote_identifiers(query, dialect=dialect)
def interpret_expression( e: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr | str | int | float | bool:
1717def interpret_expression(e: exp.Expr) -> exp.Expr | str | int | float | bool:
1718    if e.is_int:
1719        return int(e.this)
1720    if e.is_number:
1721        return float(e.this)
1722    if isinstance(e, (exp.Literal, exp.Boolean)):
1723        return e.this
1724    return e
def interpret_key_value_pairs( e: sqlglot.expressions.query.Tuple) -> Dict[str, sqlglot.expressions.core.Expr | str | int | float | bool]:
1727def interpret_key_value_pairs(
1728    e: exp.Tuple,
1729) -> t.Dict[str, exp.Expr | str | int | float | bool]:
1730    return {i.this.name: interpret_expression(i.expression) for i in e.expressions}
def extract_func_call( v: sqlglot.expressions.core.Expr, allow_tuples: bool = False) -> Tuple[str, Dict[str, sqlglot.expressions.core.Expr]]:
1733def extract_func_call(
1734    v: exp.Expr, allow_tuples: bool = False
1735) -> t.Tuple[str, t.Dict[str, exp.Expr]]:
1736    kwargs = {}
1737
1738    if isinstance(v, exp.Anonymous):
1739        func = v.name
1740        args = v.expressions
1741    elif isinstance(v, exp.Func):
1742        func = v.sql_name()
1743        args = list(v.args.values())
1744    elif isinstance(v, exp.Paren):
1745        func = ""
1746        args = [v.this]
1747    elif isinstance(v, exp.Tuple):  # airflow only
1748        if not allow_tuples:
1749            raise ConfigError("Audit name is missing (eg. MY_AUDIT())")
1750
1751        func = ""
1752        args = v.expressions
1753    else:
1754        return v.name.lower(), {}
1755
1756    for arg in args:
1757        if not isinstance(arg, (exp.PropertyEQ, exp.EQ)):
1758            raise ConfigError(
1759                f"Function '{func}' must be called with key-value arguments like {func}(arg := value)."
1760            )
1761        kwargs[arg.left.name.lower()] = arg.right
1762    return func.lower(), kwargs
def extract_function_calls(func_calls: Any, allow_tuples: bool = False) -> Any:
1765def extract_function_calls(func_calls: t.Any, allow_tuples: bool = False) -> t.Any:
1766    """Used for extracting function calls for signals or audits."""
1767
1768    if isinstance(func_calls, (exp.Tuple, exp.Array)):
1769        return [extract_func_call(i, allow_tuples=allow_tuples) for i in func_calls.expressions]
1770    if isinstance(func_calls, exp.Paren):
1771        return [extract_func_call(func_calls.this, allow_tuples=allow_tuples)]
1772    if isinstance(func_calls, exp.Expr):
1773        return [extract_func_call(func_calls, allow_tuples=allow_tuples)]
1774    if isinstance(func_calls, list):
1775        function_calls = []
1776        for entry in func_calls:
1777            if isinstance(entry, dict):
1778                args = entry
1779                name = "" if allow_tuples else entry.pop("name")
1780            elif isinstance(entry, (tuple, list)):
1781                name, args = entry
1782            else:
1783                raise ConfigError(f"Audit must be a dictionary or named tuple. Got {entry}.")
1784
1785            function_calls.append(
1786                (
1787                    name.lower(),
1788                    {
1789                        key: parse_one(value) if isinstance(value, str) else value
1790                        for key, value in args.items()
1791                    },
1792                )
1793            )
1794
1795        return function_calls
1796
1797    return func_calls or []

Used for extracting function calls for signals or audits.

def is_meta_expression(v: Any) -> bool:
1800def is_meta_expression(v: t.Any) -> bool:
1801    return isinstance(v, (Audit, Metric, Model))
def replace_merge_table_aliases( expression: sqlglot.expressions.core.Expr, dialect: Optional[str] = None) -> sqlglot.expressions.core.Expr:
1804def replace_merge_table_aliases(expression: exp.Expr, dialect: t.Optional[str] = None) -> exp.Expr:
1805    """
1806    Resolves references from the "source" and "target" tables (or their DBT equivalents)
1807    with the corresponding SQLMesh merge aliases (MERGE_SOURCE_ALIAS and MERGE_TARGET_ALIAS)
1808    """
1809    from sqlmesh.core.engine_adapter.base import MERGE_SOURCE_ALIAS, MERGE_TARGET_ALIAS
1810
1811    if isinstance(expression, exp.Column) and (first_part := expression.parts[0]):
1812        if first_part.this.lower() in ("target", "dbt_internal_dest", "__merge_target__"):
1813            first_part.replace(exp.to_identifier(MERGE_TARGET_ALIAS, quoted=True))
1814        elif first_part.this.lower() in ("source", "dbt_internal_source", "__merge_source__"):
1815            first_part.replace(exp.to_identifier(MERGE_SOURCE_ALIAS, quoted=True))
1816
1817    return expression

Resolves references from the "source" and "target" tables (or their DBT equivalents) with the corresponding SQLMesh merge aliases (MERGE_SOURCE_ALIAS and MERGE_TARGET_ALIAS)